December 10, 2003
J-Sim is an application development environment based on the component-based software architecture, Autonomous Component Architecture or ACA. This tutorial serves two purposes: first, it gives you an overview of the component-based architecture; second, it teaches you how to write components and compose components with the Tcl script language. We assume that you are reasonably familiar with Java(TM) and Tcl, have installed J-Sim on your computer and know how to start the terminal in J-Sim.
Scripting is commonly used in a large software environment for users to access and manipulate components at the desirable granularity. Scripting is an essential part of J-Sim, for we use it to "glue" all the components and define how the system operates. We have developed a scripting interface framework to incorporate different script languages in J-Sim. However, at the time of writing this tutorial, only Tcl is fully supported in J-Sim. In what follows, we use Tcl as the example script language.
Tcl/Java, made available by third parties, is an extension to the core Tcl. It makes it possible to manipulate Java objects in the Tcl environment. We integrate the pure-Java implementation of Tcl8.0 with Tcl/Java, called Jacl, in our toolkit1 Tcl/Java defines a set of new Tcl commands for manipulating Java objects, such as creating an object from a Java class, invoking a method of a Java object, or accessing a field variable of a Java object. A list of Tcl/Java commands can be found in the online manuals at the Tcl/Java website.
1Making the whole system run only in Java facilitates the debugging process. If the performance of Tcl is more of an issue, one may consider using TclBlend. This, however, is not covered in this tutorial.
We use the following example to illustrate the use of Tcl/Java and our
terminal environment. First, run J-Sim. A terminal named "TCL0" will
pop up. (The terminal created using JDK1.3 in Windows is
shown below.)
The terminal consists of the display area (the broad area in the center) and the input area (the one-line input at the bottom). The commands are typed in in the input area and the results are displayed in the display area. The terminal also provides several simple but useful functions in the pull-down menu. For example, you can save the current session into a file for later use.
Now to play a little with Tcl/Java, type the following commands in the terminal:
set f [java::new java.awt.Frame "Tcljava"]
set l [java::new java.awt.Label "Hello World!"]
$l setFont [java::new {java.awt.Font String int int} "TimesRoman" [java::field java.awt.Font BOLD] 40]
$f add $l [java::field java.awt.BorderLayout CENTER]
$f pack
$f show
You will see the "Hello World!" string displayed in a standard Java AWT
frame.
To understand how the above Tcljava commands work, we show below the corresponding Java codes:
java.awt.Frame f = new java.awt.Frame("Tcljava");
java.awt.Label l = new java.awt.Label("Hello World!");
l.setFont(new java.awt.Font("TimesRoman", java.awt.Font.BOLD, 40);
f.add(l, java.awt.BorderLayout CENTER);
f.pack();
f.show():
To help you understand TclJava a little further, here is a quick and dirty
summary of Tcl/Java commands. (The detailed syntax of TclJava commands can
be found here.)
java::new to create new Java objects.
java::new returns a reference to the newly-created Java object. The reference
can be used as a (new) Tcl command for users to invoke methods, or access
field variables, of the object.puts $f" to find out the reference to the frame which we just
created. Suppose it is java0x2. With this reference, you may close and
open the frame:
java0x2 hide java0x2 show
As you may have figured out, a Java object reference in TclJava is a Tcl command.
Caution: You should keep references to Java objects in Tcl variables
in order for them to be alive. Otherwise, the reference will be removed
and no longer be a valid Tcl command, and the referenced object is lost.
(The referenced object will be garbage collected or hanging somewhere in
the memory.) To verify the above, you can type "unset f"
and re-enter the
above commands, and you should get a Tcl exception.
To conclude this section, let's close and dispose the window by "$f
dispose".
Before we start writing a component, let's take a short tour of the component-based architecture.
The basic entity in the software architecture is a component. We envision an application as a composition of components. The notion of components is not new, and has been used in several commercial component-based software standards such as JavaBeans, CORBA and COM/DCOM/COM+. Unlike JavaBeans, CORBA, and COM/DCOM, the components in J-Sim are loosely coupled, communicate with one another by "wiring" their ports together, and are bound to contracts. Contracts specify the causality of data sent/received between components, but do not specify the components that participate in the communication. Contracts are bound at design time and components are bound at system integration time. One immediate advantage of this separation is that different components can be independently developed (on different platforms and/or different programming languages2) and integrated later.
2Currently
we only implement ACA in Java. At the time of writing this tutorial, we have
not developed supporting toolkits for the other programming languages/platforms,
due to the limited resources ($$$).
. |
Port: A component communicates with the rest of the world via its ports. A component may own more than one port. The programming interface between a component and its port is well defined. Since a component only interfaces with its ports, one component can be developed without the existence of other components. Also, the actual communication mechanism a component uses to communicate with the rest of the world is completely hidden in ports.
Contract: The behavior of a component is described by the port contract and the component contract. A port contract is bound to a specific port or a group of ports, defines the communication pattern between the component that owns the port(s), and the other components that are connected to the port(s). The component contract is the same as the traditional blackbox specification and characterizes the input/output relation of a component. A component is expected to work properly if all the adopted contracts are fulfilled.
It should be clear now that when a user writes a component, s/he has only to follow the contracts adopted by the component, but need not worry about the other components or the communication mechanism between them.
A good analogy of the component-based architecture is the current IC
architecture, where a hardware module (a software system) is assembled
by connecting a set of IC chips (components) through their pins (ports).
When the signals arrive at the pins of an IC chip, the chip performs certain
tasks in compliance with the specification in the cookbook (contract),
and may send signals at some other pins. A component can be reused
in other software systems with the same contract context, in much the same
fashion as IC chips are used in hardware design.
Ports in a component can be organized into different groups. A port group is uniquely identified within a component by its group ID. A port is uniquely identified within a port group by its assigned port ID. Therefore, a port is uniquely identified within a component by its port group and port IDs. A component has a default port group with empty ID ("").
The ultimate goal of the component architecture is to mimic the current
hardware manufacture architecture: a rich set of IC chips are available in the market. By selecting and connecting an appropriate set of chips,
one can readily compose a hardware component with desirable functions.
An important step towards the goal is to build a set of components that
can be re-used in applications of similar nature. In the following sections,
we show how to create a component from scratch and how to assemble components
into an application in J-Sim.
|
|
![]() |
Now we write a simple component that owns a "greetings" port.
As shown in Figure 2, the component expects to receive "hi" from the outside
world through the port and responds with "Hello World!". The code is listed
below, followed by explanations.
Component: HelloWorld 1 /**
2 * A simple Hello World component.
3 * The component contains a "greetings" port which responds with "Hello World
4 * when it receives "hi".
5 */
6 public class HelloWorld extends drcl.comp.Component
7 {
8 drcl.comp.Port greetingsPort;
9
10 public HelloWorld()
11 {
12 super("hello");
13 greetingsPort = addPort("greetings");
14 }
15
16 public void process(Object data_,
17 drcl.comp.Port inPort_)
18 {
19 if (inPort_ == greetingsPort && "hi".equals(data_))
20 inPort_.doSending("Hello World!\n");
21 }
22 }
|
6 The class extends drcl.comp.Component, the base class of all components.
13 The port to receive greetings from the outside world is added.
16 The process method is invoked as a callback function when data is received at one of the component's ports.
17 inPort_ is the port at which data_ arrives.
20 The component sends "Hello World!" at inPort_. |
Let's try out the HelloWorld component. After compiling the class,
run J-Sim in the same directory in which the class resides. In the terminal,
type the following commands:
Example
1: Hello World!1 set c [java::new HelloWorld] 2 [$c getPort "greetings"] connectTo [! /.term/tcl0/result@] 3 java::call drcl.comp.Util setRuntime $c [java::new drcl.comp.ARuntime] 4 java::call drcl.comp.Util inject "hi" [$c getPort "greetings"] |
1 The HelloWorld component is created.
2 The greetings port is connected to the Tcl terminal so that
the "Hello World!" is sent to and printed on the terminal. We will come
back to explain [! /.term/tcl0/result@] in the section of RUV
system.
3 Before we can use the component, we must attach a runtime to it. The drcl.comp.ARuntime class provides a default runtime implementation.
4 A greeting message is "injected" to the component at its greetings
port by the inject() method of the class drcl.comp.Util.
(Note that getPort() and connectTo() are the methods
in the component class in J-Sim. See here
for a list of methods in the component class. |
You should see the greetings on the terminal!
What's going on here: A component
is activated when it receives data. The incoming data is processed in the
callback method process(Object data_, Port inPort_) in a new thread context.
It is possible for a component to handle multiple data simultaneously,
i.e., multiple threads may be active in the same component at the same
time. So a component writer must be familiar with multithreaded
programming. However, a component writer only needs to pay attention to
the case where different threads may access
some shared data structures in the component. It is the component writer's responsibility to
ensure the integrity of the shared data being accessed by multiple threads
simultaneously.
In J-Sim, a (parent) component may include several
sub-components (called
child components). Figure 3 illustrates the concept.
A component is uniquely identified within its parent component by its ID.
![]() |
Ports of a child component or the child component itself may be exposed to the outside world of its parent. Port exposure is realized by creating a port for the parent and then connecting it to the port of the child. The port of the parent component acts as a shadow port of that of the child component. The real communication occurs between the outside world and the child component's port.
Child exposure is realized by (i) creating a shadow port of the parent for every port of the child when the child is included in the parent; and (ii) creating a port of the child when a port of the parent is created and making the parent's port a shadow of of the child's port.
We use the following two scripts to illustrate the concept of port/child
exposure.
Example
2: Port Exposure1 set c [java::new HelloWorld] 2 set parent [java::new drcl.comp.Component] 3 $parent addComponent $c 4 $parent exposePort $c "greetings" 5 [$parent getPort "greetings"] connectTo [! /.term/tcl0/result@] 6 java::call drcl.comp.Util setRuntime $parent [java::new drcl.comp.ARuntime] 7 java::call drcl.comp.Util inject "hi" [$parent getPort "greetings"] |
1 The component HelloWorld is created.
2 The parent component is created.
3 The HelloWorld component is added to the parent component as a child
component.
4 The greetings port of the HelloWorld component is exposed to the
parent.
5 The greetings port of the parent component is connected to the terminal.
6 Before we can use the components, we must attach a runtime to it. The drcl.comp.ARuntime class provides a default runtime implementation. Note that the runtime instance is propagated to the component hierarchy rooted at $parent (including both the parent and the child components).
7 A greetings message is sent to the greetings port of the parent (and
received by the child which in turn responds with "Hello World!") |
Example
3: Child Exposure1 set c [java::new HelloWorld] 2 set parent [java::new drcl.comp.Component] 3 $parent addComponent $c 4 java::call drcl.comp.Util setRuntime $parent [java::new drcl.comp.ARuntime] 5 $parent expose $c 6 [$parent getPort "greetings"] connectTo [! /.term/tcl0/result@] 7 java::call drcl.comp.Util inject "hi" [$parent getPort "greetings"] |
1 The HelloWorld component is created.
2 The parent component is created.
3 The HelloWorld component is added to the parent as a child component.
4 Before we can use the components, we must attach a runtime to it. The drcl.comp.ARuntime class provides a default runtime implementation. Note that the runtime instance is propagated to the component hierarchy rooted at $parent (including both the parent and the child components).
5 The child component HelloWorld is exposed (which in turn exposes
the greetings port of HelloWorld.)
6 The greetings port of the parent is connected to the terminal.
7 A greetings message is sent to the greetings port of the parent (and
received by the child which in turn responds with "Hello World!") |
When you execute either example, you should see the greetings message
displayed on the terminal.
In the course of developing a large software project, it may become
cumbersome to use many of the Tcl/Java commands because one has to store
the references of the Java objects in Tcl variables in order to access
them. Naming of these Tcl variables is not at all trivial. For example,
in the component hierarchy, a Tcl variable can only be used to access one
component in the hierarchy and the other components/ports have to be accessed
by using methods like getParent(), getComponent()3
and getPort().
To mitigate the referencing problem, we construct a referencing system,
called RUntime Virtual system or RUV in short, on top of Tcl/Java. Because
the component hierarchy resembles in essence the UNIX file system hierarchy,
we employ the same notation and represent a component or a port as a path,
in the same manner a file is represented in a file system. For example,
/component1/child2 represents the component with ID child2 within the component
represented by /component1; /component3/port3@group4 represents the port
with ID port3 in the port group of ID group4 within the component represented
by /component3. Note that we use "@" in a port path to separate the port
ID and the port group of the port.
Moreover, we provide several UNIX-file-system-like commands, such as ls,
cd, pwd, mkdir, cp, mv,
rm and cat in the context of the component
hierarchy to navigate a component system and manipulate components and ports in
the system. The commands are summarized
in Table 1.
|
4 Following the convention in the UNIX file system, a hidden object is a component or port whose name begins with ".".
The following example illustrates how to use RUV commands to accomplish
the same task in Example 1. (Note that commands
are typed after the prompt "TCL0>", and lines without the prompt are the
results returned.
Example
4: RUV System1 TCL0> mkdir HelloWorld hello 2 TCL0> ! /hello/greetings@ connectTo [! /.term/tcl0/result@] 3 TCL0> java::call drcl.comp.Util inject "hi" [! /hello/greetings@] 4 Hello World! 5 TCL0> inject "hi" hello/greetings@ 6 Hello World! |
1 The component with ID "hello" is created..
2 The greetings port of the hello component is connected to the terminal.
3 A greetings message is injected to the hello component.
5 A greetings message is again injected to the hello component using the relative
path and the "inject" Tcl command. |
Now we can explain what /.term/tcl0/result@ means in all these examples:
the terminal tcl0 is itself a component! It is stored in the
/.term/ "directory"
when the system is brought up. The terminal contains a "result" port in
the default port group. Whenever the terminal receives data at this port,
it displays the data. When we send "hi" to the greetings port of hello
component, the component responds with "Hello World!" which is forwarded
to the result port of the terminal and displayed on the terminal.
Note that we do not attach any runtime to the component as we did in previous examples because the root component of the RUV system has the default runtime instance attached to it and the runtime instance is automatically propagated to child components when they are added to the parent component.
RUV has powerful path expressions. In addition to the wildcard expressions
using "*" and "?", one may use the range expression "<ID>-<ID>" that
specifies a range of components. For example, "n3-n10" specifies the components
"n3", "n4", ..., and "n10". One may also use exhaustive expression
"..."
that specifies all the components that match the partial path. For example,
USA/.../Columbus would match USA/Ohio/Columbus, USA/Georgia/Columbus,
USA/Indiana/Columbus,
USA/Mississippi/Columbus and USA/Nebraska/Columbus if all the states and
cities are built in the USA component hierarchy. Another possible path
expression is to use the component instance at the start of the path, as
illustrated in the following example (this is the most succinct version
of all the HelloWorld example in this tutorial):
Example 5: Path Expression1 TCL0> set c [mkdir HelloWorld hello] 2 TCL0> connect $c/greetings@ -to /.term/tcl0/result@ 3 TCL0> inject "hi" $c/greetings@ 4 Hello World! |
For more about path expressions, please refer to the RUV commands reference page.
In what follows, we continue the above example and illustrate how to
use other RUV commands.
Now we are in a position to discuss how to write a component. We provide
the following example which may serves a template for writing a complete
and correct component. We also give the guidelines for documentation and
several rules which a component writer must comply with. Essentially
one must override one method, process(), and may override six other methods,
reset(), duplicate(), info() and _start()/_stop()/_resume()
in a component.
Component:
The Template 1 import drcl.comp.*;
2
3 /**
4 * Template for writing a component.
5 * When data arrives at one of the ports, this component responds with the data received last time.
6 * That is, when the component receives the (i+1)th data at inPort_, it sends the ith data at inPort_.
7 */
8 public class ComponentTemplate extends drcl.comp.Component
implements drcl.comp.ActiveComponent
9 {
10 // The fields here are simply defined to demonstrate how
11 // methods in this template are used.
12 int x;
13 Object lock;
14
15 /**
16 * Constructor.
17 */
18 public ComponentTemplate()
19 {
20 super();
21 }
22
23 /**
24 * Constructor.
25 */
26 public ComponentTemplate(String id_)
27 {
28 super(id_);
29 }
30
31 /**
32 * Invoked when data_ arrives at this component at the inPort_ port.
33 */
34 protected void process(Object data_, drcl.comp.Port inPort_)
35 {
36 // Put codes here for handling the incoming data.
37 synchronized (lock) {
38 x++;
39 notify(lock); // Notifies previously-waiting thread.
40 wait(lock); // Delays sending data until someone notifies.
41 inPort_.doSending(data_);
42 }
43 }
44
45
46 /**
47 * Resets this component to the initial state for use anew.
48 * Must call super.reset() in the beginning.
49 */
50 public void reset()
51 {
52 super.reset(); // Let super class reset its fields.
53 x = 0; // Reset the fields defined in this class.
54 }
55
56 /**
57 * Copies the content from the source_ to this component.
58 * Must call super.duplicate() in the beginning.
59 */
60 public void duplicate(Object source_)
61 {
62 super.duplicate(source_); // Let super class copy its fields.
63 ComponentTemplate that_ = (ComponentTemplate)source_;
64 x = that_.x; // Duplicate the fields defined in this class.
65 // Suppose we don't have to copy lock.
66 }
67
68 /**
69 * Invoked when the component is run()ed.
70 */
71 protected void _start()
72 {
73 debug(this + " is starting!");
74 }
75
76 /**
77 * Script interface which reveals the internal states of the component.
78 * It is for debugging and demonstration purpose.
79 */
80 public String info()
81 {
82 return "Current count = " + x + "\n";
83 }
84
85 /**
86 * Script interface which increments the counter by +1.
87 */
88 public void increment()
89 {
90 x++;
91 }
92 }
|
process(), line 34) process() method is the heart of the component and implements the
behavior of the component. Specifically, the method is invoked by the system
whenever some data arrives at one of the component's ports. As mentioned
earlier, the method is executed in a new
thread context. It is possible for a component to handle multiple data
simultaneously in multiple threads. It is component writer's responsibility
to ensure maintain data integrity and synchronization among multiple threads.
For example, the code block in lines 37-42 is protected by the keyword synchronized, which imposes the restriction that only one thread is allowed
to access the code block at any time.
reset(), line 50)reset() method sets the component to its initial state. It allows
the component to be started anew after being executed for some time. To
correctly override the method, the subclass must call super.reset(). When
recursive calling of the super.reset() method reaches drcl.comp.Component,
the method resets the ports and the child components in a recursive
manner.
duplicate(), line 60)duplicate() method allows the component to be clone()d. A subclass
should override this method to copy the fields defined in the subclass.
When overriding the method, the subclass must call super.duplicate() so
that the super class can copy the fields defined in it. When recursive
calling of the super.duplicate() method reaches drcl.comp.Component., the
method duplicates the ports and the child components in a recursive manner,
and then connects the child component in the same manner as in the source
component. The method originates from the drcl.ObjectDuplicable interface.
It complements the clone mechanism that exists in the java.lang.Object.
The RUV system command cp uses this method to do the tricks.
_start() line 71) run() method. The run() method creates a new thread
and then the thread calls the _start() method of the component. The
run() method also calls the run() method of the child components, until all the
components in the hierarchy are _start()ed. As not all the components
in the hierarchy need to be _start()ed, we provide the drcl.comp.ActiveComponent
interface. In the run() process mentioned above, only when a component
implements the interface will a new thread be created which in turn calls
the_start() method of the component.
info()
line 80) increment() in line 88) to manipulate the
component from the scripting environment (e.g. in a Tcl terminal). However,
as viewed from a component, the script interfaces of the other components
can not be accessed because by virtue of the loosely coupled component
architecture, these interfaces are blocked by ports. As a component
is interfaced with ports, it does not know to which components it is connected
and cannot readily access the interfaces in other components (review).
wait(Object)/notify(Object)/notifyAll()
in drcl.comp.Component in replace of wait()/notify()/notifyAll() in
java.lang.Object. The semantics of thread synchronization are
still the same as in Java.
Please refer to Sun's
tutorial for more information on thread synchronization.For more details regarding how to write a component, please refer to the Component Writer's Guide.
* JAVA and all JAVA-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the U.S. and other countries.