HelloWriter, which will write an Announcement object to working memory. The first step is to create the header and source files for the component in our c++ source directory. Create the files HelloWriter.hpp and HelloWriter.cpp under src/c++. Next, edit HelloWriter.hpp to contain:
#ifndef HELLO_WRITER_HPP_ #define HELLO_WRITER_HPP_ #include <cast/architecture.hpp> class HelloWriter : public cast::ManagedComponent { }; #endif
This creates a component as a subclass of cast::ManagedComponent (the most commonly used CAST component that can read and write to WM) but does nothing else. To complement this, edit HelloWriter.cpp to contain:
#include "HelloWriter.hpp" extern "C" { cast::CASTComponentPtr newComponent() { return new HelloWriter(); } }
This file first includes the header, then defines a function which will create a new instance of our component (returning the result via an Ice smart pointer). This function is very important as it's the only way that the CAST component framework knows how to create an instance of this component.
Before we go any further, let's compile this component to check that everything's working so far. To do this we will use CMake (this is overkill for a single component, but will be useful as we add additional c++ components). To do this means adding the component into the cmake build process. The easiest way to do this is to copy the CAST template files as follows. First we need to make our subarchitecture a new cmake project.
To do this, first copy the "top level" cmake file TopLevelTemplateCMakeLists.txt from CAST's template directory (the install prefix + share/cast/templates, e.g. /usr/local/share/cast/templates)into $TUTORIAL_ROOT and rename to CMakeLists.txt. E.g.
cp /usr/local/share/cast/templates/TopLevelTemplateCMakeLists.txt $TUTORIAL_ROOT/CMakeLists.txt
Next, edit this new file, replacing MYPROJECT with the name of your project (e.g. CASTTutorial). Then include our new subarchitecture in this project. To do this, edit CMakeLists.txt again to add the following line at the end:
add_subdirectory (subarchitectures/hello-world)
Next we must tell CMake that some content exists in the hello-world directory. To do this copy ProjectTemplateCMakeLists.txt from CAST's template directory into hello-world and rename it to CMakeLists.txt. E.g.
cp /usr/local/share/cast/templates/ProjectTemplateCMakeLists.txt $TUTORIAL_ROOT/subarchitectures/hello-world/CMakeLists.txt
Then edit the new CMakeLists.txt file and replace MYPROJECTNAME with HelloWorld. Next we need to include our component in this new project. To do this first edit our new CMakeLists.txt to include the line add_subdirectory(src/c++). Next copy ComponentTemplateCMakeLists.txt from from CAST's template directory into our c++ directory and rename to CMakeLists.txt. E.g.
cp /usr/local/share/cast/templates/ComponentTemplateCMakeLists.txt $TUTORIAL_ROOT/subarchitectures/hello-world/src/c++/CMakeLists.txt
Then edit this new file and replace MYCOMPONENTNAME with HelloWriter. This latter file tells cmake to create a new shared library called libHelloWriter.so which CAST will use to create an instance of our new component when we run the system.
Finally we have to run CMake to create Makefiles, then actually compile the project. For our own sanity we create our Makefiles and build in a separate directory called BUILD. Create this and change into it:
mkdir $TUTORIAL_ROOT/BUILD cd $TUTORIAL_ROOT/BUILD
Next run the ccmake executable on the directory containing the toplevel CMakeLists.txt file. E.g.
cd $TUTORIAL_ROOT/BUILD ccmake ..
This command will produce a GUI window with a number of options. The default values should be fine. To create the build files press 'c' (to configure the build files) until you are able to press 'g' (to generate the build files). If you encounter errors, follow the instructions in the error messages to fix them.
All that remains now is to build the project. Do this by making the install target in the BUILD directory. E.g.
cd $TUTORIAL_ROOT make -C BUILD install
If this has been successful you should see a new file called $TUTORIAL_ROOT/output/lib/libHelloWriter.so.
To complete our sanity check we should run our new component and see what happens. As the component doesn't do anything nothing should happen, but a successful nothing is better than an unsuccessful something! CAST systems are run using clients and servers, and are configured using a file called a CAST file which describes which components to run on which machines. Let's create a basic CAST file called subarchitectures/hello-world/config/helloworld.cast and enter the following lines:
HOST localhost SUBARCHITECTURE hello-world CPP WM SubarchitectureWorkingMemory CPP TM AlwaysPositiveTaskManager CPP MG writer HelloWriter
The first line defines the default host one which the components should be run. For this you can try the name of your machine or "localhost". After that we define a subarchitecture. The first two lines in this construct define the component classes used for the subarchitecture working memory (WM) and task manager (TM). The example above uses built-in components for these roles. The final line in the subarchitecture definition introduces our new component. The first part of the line tells CAST what language the component is written in. The second part tells CAST that it's a managed component (i.e. the class we inherited for our component). The third part defines a unique ID we can use to refer to our component. And the final part is the library that CAST will load to instantiate our component (i.e. libHelloWriter.so from which it uses the newComponent function). For more information see the configuration instructions. Now we've defined our system we must can run it. Open two terminals and change directory to $TUTORIAL_ROOT in each of them.
Before you run the server, you must make sure that your library path contains the directory where libHelloWriter.so is located (so your component can be instantiated at run-time). The simplest way to do this is to set the environment variable LD_LIBRARY_PATH (or DYLD_LIBRARY_PATH on OS X) to include the directory which contains your library. E.g. (on Linux in bash)
export LD_LIBRARY_PATH=output/lib:$LD_LIBRARY_PATH
Now we can start the server. In the first terminal (where you've set your library path variable), run the cast-server executable. This should give you some output like the following:
vonnegut:tutorial nah$ cast-server Java server: 1530 CPP server: 1531
The numbers tell you the process numbers of the underlying CAST servers. Finally run the cast-client executable, passing our new CAST file as the first argument:
vonnegut:tutorial nah$ cast-client subarchitectures/hello-world/config/helloworld.cast CoSy Architecture Schema Toolkit. Release: 0.2.0
As our component doesn't do anything, you should see much else in terms of output. To stop the system running Ctrl-C the client script. This will send appropriate shutdown signals to all running components.
Having got this far we can be pretty confident that CAST is running fine and that our new component has been accepted into its bosom. Next we can extend our component to actually do something. For our tutorial we want HelloWriter to add an Announcement object to working memory for some other component to read. To do this, we must get the component to execute some code when it runs. Every component in CAST inherits a runComponent() member function which is called when the component is run. To use this, extend your HelloWriter class header to include the following lines:
protected: virtual void runComponent();
Then add the following to your HelloWriter.cpp file:
void HelloWriter::runComponent() { println("Look out world, here I come..."); }
This code just prints out (using CAST's built in print method) a nice message so we know things are running. Recompile and run your component to verify that this works.
Next we need to create an instance of Announcement to write to working memory. We have already defined this object in Slice, but we now need to use this in C++. To do this we must use Ice's slice2cpp program to generate C++ source code from our Slice definition file. Although you could do this by hand, CAST has a built-in cmake macro to do this for you (borrowed from the lovely folk at Orca). Open SliceLibraryTemplateCMakeLists.txt from CAST's template directory, and paste the contents at the top of your component's CMakeLists.txt file, replacing MYSLICEFILE with HelloWorldData as you go. E.g. we should change the file to:
include_directories(.)
include(Slice2Cpp)
cast_slice2cpp(GEN_CPP GEN_HPP HelloWorldData.ice)
add_library(HelloWorldData SHARED ${GEN_CPP})
install(TARGETS HelloWorldData LIBRARY DESTINATION lib ARCHIVE DESTINATION lib)
add_cast_component(HelloWriter HelloWriter.cpp)
This will create HelloWorldData.hpp and .cpp containing the C++ version of our slice file, and from this create a library called libHelloWorldData.so when these files are compiled. As we will use this compiled code in our component, we should also tell cmake to link our component with this library. To do this, add the following line at the end of the file:
link_cast_component(${CAST_COMPONENT_NAME} HelloWorldData)
Where ${CAST_COMPONENT_NAME} is a variable set by add_cast_component that contains the name of the last component added. Using this variable makes the cmake files a little easier to maintain as things get more complex later on. Once this is complete, reconfigure (by running ccmake as before) and rebuild your component (by running make as before). You should now see some output about "Generating source" and some extra compilations.
With this step complete we can now finally create an instance of {{Announcement}} to write to working memory. First off, include the newly generated HelloWorldData.hpp in HelloWriter.hpp:
#include <HelloWorldData.hpp>
Then, in runComponent() create an instance of Announcement with the message you want to send:
void HelloWriter::runComponent() { println("Look out world, here I come..."); helloworld::AnnouncementPtr ann = new helloworld::Announcement("Hello World!"); }
The most important things to note about the addition line is that the object is allocated on the heap (i.e. using new) rather than on the stack, and the result of the allocation is stored using a smart (reference-counted) pointer (the name of the class with the postfix Ptr). This is purely an Ice constraint, but it must be obeyed for all Slice class types, and therefore for all instances of classes that are to be written to working memory in CAST. For more information on why this is the case read this.
The only remaining task for this component is to write the newly created object to working memory. All working memory entries are created with an ID string which should ideally be generated using newDataID(). The ID and the object are then sent to working memory via a call to addToWorkingMemory as demonstrated in the final line of runComponent:
void HelloWriter::runComponent() { println("Look out world, here I come..."); helloworld::AnnouncementPtr ann = new helloworld::Announcement("Hello World!"); addToWorkingMemory(newDataID(), ann); }
Now we have our writer component we need HelloReader to read the announcement that has been written to WM. Create the component by copying HelloWriter, but remove the runComponent() method (as we don't need it for this component). Next, edit the CMakeLists.txt file to add a new component called HelloReader, i.e. add
add_cast_component(HelloReader HelloReader.cpp)
link_cast_component(${CAST_COMPONENT_NAME} HelloWorldData)
Compile the code to check that this new component is correct.
When data on working memory is changed (e.g. when a new Annoucement object is written to it) the working memory generates a new Working Memory Change event which is sent to all components that listening for such events. So, the first thing our HelloReader must do is register with the working memory to listen for the events it cares about. When a component is started up, before runComponent() is called (but after the constructor and configure), each component's start member function is called. It is here we will tell the WM what change events we're interested in. We do it here because it guarantees that we'll see any changes that are created by other components' runComponent methods, because start is always called before runComponent (for more information on methods are called when, see System Architecture).
To do this, we first add the start method to HelloReader.hpp and .cpp:
protected: virtual void start();
void HelloReader::start() {
}
Next we need to tell working memory what we're interested in. We do this by passing CAST a WorkingMemoryChangeFilter which describes what WM changes the component is interested in, and a WorkingMemoryChangeReceiver which is a callback object that is called when events matching the filter occur. Although you can create change filters by hand, the file ChangeFilterFactory (included via the previously inclusion of architecture.hpp) provides helper functions to create most commonly required filter types. For our HelloReader component add the following line to the start function:
addChangeFilter(cast::createLocalTypeFilter<helloworld::Announcement>(cast::cdl::ADD), new cast::MemberFunctionChangeReceiver<HelloReader>(this, &HelloReader::makeAnnouncement));
This does the following things. cast::createLocalTypeFilter<helloworld::Announcement>(cast::cdl::ADD) creates a working memory change filter that listens for additions to working memory (cast::cdl::ADD) of the Announcement class (the template argument) in the working memory of this component's subarchitecture (cast::createLocalTypeFilter). If you used cast::createGlobalTypeFilter you could listen for matching events on any working memory in the whole system. The second argument to addChangeFilter creates a new change receiver object that automatically calls a member function in a class. In this case we've told it to call the member function makeAnnouncement which we've yet to define. So, next we'll define this. In order to be called when a change occurs, the function must have a particular signature. It must return void and must accept and a single const WorkingMemoryChange reference, i.e. (leaving the header as an for the reader):
void HelloReader::makeAnnouncement(const cast::cdl::WorkingMemoryChange & _wmc) { }
The input argument, WorkingMemoryChange contains a number of fields which describe the event that has occurred (these are matched to the filter to determine whether the change event should be received or not). We can use one of these, the address field, to access the changed data on working memory. The address is of type WorkingMemoryAddress which contains an id field and a subarchitecture field. In this case id is the string generated by HelloWriter using newDataID(). The subarchitecture field will contain the name of the current component's subarchitecture. This is "hello-world" as described above in the CAST file. With the address from the change event we can read the data from working memory. There are many ways to do this, but the easiest is to use getMemoryEntry. This accepts an address and returns an Ice smart pointer to the requested object. To use this, add the following to makeAnnouncement:
helloworld::AnnouncementPtr ann = getMemoryEntry<helloworld::Announcement>(_wmc.address);
And finally we can print out our announcement by adding:
println("I'd like to make an announcement: %s", ann->message.c_str());
At this point we have completed our code, but need to add our new component into our system to test it. To do this, add the following extra line to the end of the helloworld.cast CAST file:
CPP MG reader HelloReader
With this in place you should be able to re-run the system and see (in the server terminal):
CoSy Architecture Schema Toolkit. Release: 0.2.0 ["writer": Look out world, here I come...] ["reader": I'd like to make an announcement: Hello World!]
If you see this, congratulations, you've written your first CAST system :)
1.5.8