We refer to these environments and debuggers as "clients".
The details and motivation behind the development of the SML language are described in the "Soar XML Interface Specification" which goes into a lot more depth on the details of the XML dialect. However, for users this guide may be largely sufficient.
We provide a series of classes that together hide the details of the XML messaging system while allowing the client full control over Soar. This guide will provide a quick introduction to using those classes.
The SML API is natively implemented in C++, but there are Java, Python, and Tcl bindings automatically generated by SWIG. Although all examples used in this document are in C++, it should be fairly intuitive how they translate into other languages. Later in the document is a section containing example environments written in Java.
This page explains how to compile SML clients in various languages.
Simple SML example
This example is something of a "hello world" example of how to use the major elements of the SML API. Once you understand this example, you'll be pretty much ready to dive in.
Code:
// Generally only need this one header file #include "sml_Client.h" using namespace sml ; using namespace std ; void main() { // Create an instance of the Soar kernel in our process Kernel* pKernel = Kernel::CreateKernelInNewThread() ; // Check that nothing went wrong. We will always get back a kernel object // even if something went wrong and we have to abort. if (pKernel->HadError()) { cout << pKernel->GetLastErrorDescription() << endl ; return ; } // Create a Soar agent named "test" // NOTE: We don't delete the agent pointer. It's owned by the kernel sml::Agent* pAgent = pKernel->CreateAgent("test") ; // Check that nothing went wrong // NOTE: No agent gets created if there's a problem, so we have to check for // errors through the kernel object. if (pKernel->HadError()) { cout << pKernel->GetLastErrorDescription() << endl ; return ; } // Load some productions pAgent->LoadProductions("testsml.soar") ; if (pAgent->HadError()) { cout << pAgent->GetLastErrorDescription() << endl ; return ; } Identifier* pInputLink = pAgent->GetInputLink() ; // Create (I3 ^plane P1) (P1 ^type Boeing747 ^speed 200 ^direction 50.5) on // the input link. (We don't own any of the returned objects). Identifier* pID = pAgent->CreateIdWME(pInputLink, "plane") ; StringElement* pWME1 = pAgent->CreateStringWME(pID, "type", "Boeing747") ; IntElement* pWME2 = pAgent->CreateIntWME(pID, "speed", 200) ; FloatElement* pWME3 = pAgent->CreateFloatWME(pID, "direction", 50.5) ; // Send the changes to working memory to Soar // With 8.6.2 this call is optional as changes are sent automatically. pAgent->Commit() ; // Run Soar for 2 decisions pAgent->RunSelf(2) ; // Change (P1 ^speed) to 300 and send that change to Soar pAgent->Update(pWME2, 300) ; pAgent->Commit() ; // Run Soar until it generates output or 15 decision cycles have passed // (More normal case is to just run for a decision rather than until output). pAgent->RunSelfTilOutput() ; // Go through all the commands we've received (if any) since we last ran Soar. int numberCommands = pAgent->GetNumberCommands() ; for (int i = 0 ; i < numberCommands ; i++) { Identifier* pCommand = pAgent->GetCommand(i) ; std::string name = pCommand->GetCommandName() ; std::string speed = pCommand->GetParameterValue("speed") ; // Update environment here to reflect agent's command // Then mark the command as completed pCommand->AddStatusComplete() ; // Or could do the same manually like this: // pAgent->CreateStringWME(pCommand, "status", "complete") ; } // See if anyone (e.g. a debugger) has sent commands to Soar // Without calling this method periodically, remote connections will be ignored if // we choose the "CreateKernelInCurrentThread" method. pKernel->CheckForIncomingCommands() ; // Create an example Soar command line std::string cmd = "excise --all" ; // Execute the command char const* pResult = pKernel->ExecuteCommandLine(cmd.c_str(),pAgent->GetAgentName()) ; // Shutdown and clean up pKernel->Shutdown() ; // Deletes all agents (unless using a remote connection) delete pKernel ; // Deletes the kernel itself } // end main
Creating the kernel
Code:
// Create an instance of the Soar kernel in our process Kernel* pKernel = Kernel::CreateKernelInNewThread() ;
The local kernel can either be created in the same thread as the caller or in a new thread.
Using the same thread will generally be a bit faster, but it requires the client to periodically call pKernel->CheckForIncomingCommands() so that the kernel has a chance to check for commands coming in from other remote processes (e.g. from a debugger). So if you want maximum speed choose the current thread option, but your code will be a bit more complicated. If speed is not as critical then choose the new thread option. The speed difference isn't that large, perhaps a factor of two on the cost of making a call -- which will often be dwarfed by the cost of matching productions or updating the environment.
As you are just reading the "SML Quick Start Guide" I would strongly recommend you use CreateKernelInNewThread until you are somewhat familiar with the system. If you find you need higher performance later, switching over to the CurrentThread model later only requires changing this one line of code.
Creating input link wme's
Code:
// Create (I3 ^plane P1) (P1 ^type Boeing747 ^speed 200 ^direction 50.5) on // the input link. (We don't own any of the returned objects). Identifier* pID = pAgent->CreateIdWME(pInputLink, "plane") ; StringElement* pWME1 = pAgent->CreateStringWME(pID, "type", "Boeing747") ;
A WME's value can be updated through the pAgent->Update() method and it can be removed through pAgent->DestroyWME() which also makes the working memory object invalid.
A graph (rather than a simple tree) can be created through pAgent->CreateSharedIdWME(). This creates a new identifier WME with the same value as an existing identifier. (E.g. given pOrig = (P7 ^object O3) then CreateSharedIdWME(pInputLink, "same", pOrig) would create (I1 ^same O3)).
Committing changes
Code:
// Send the changes to working memory to Soar pAgent->Commit() ;
Running Soar
Code:
// Run Soar until it generates output or 15 decision cycles have passed pAgent->RunSelfTilOutput() ; // Run Soar for 2 decisions pAgent->RunSelf(2) ;
There's more discussion of this in Section 2 of this document.
Retrieving Output
Code:
// Go through all the commands we've received (if any) since we last ran Soar. int numberCommands = pAgent->GetNumberCommands() ; for (int i = 0 ; i < numberCommands ; i++) { Identifier* pCommand = pAgent->GetCommand(i) ; std::string name = pCommand->GetCommandName() ; std::string speed = pCommand->GetParameterValue("speed") ; // Update environment here to reflect agent's command // Then mark the command as completed pCommand->AddStatusComplete() ; // Or could do the same manually like this: // pAgent->CreateStringWME(pCommand, "status", "complete") ; }
If you choose to adopt this model for the agent's actions then it is possible to query the agent for the number of commands added since the last time Soar was run and to retrieve each Command in turn, its name and parameter values. In this example, pCommand would point to M1, the name would be move and the parameter value for speed would be 20.
If you wish to adopt a different model for agent actions, that is also supported, but the support is less direct.
First, notice that GetCommand() returns a standard Identifier WME, so this can be manipulated in the same manner as any other WME. In particular Identifier's offer GetNumberChildren and GetChild methods, so using these it is possible to start from the output link and examine all of working memory beneath the output link. There are also other methods (such as FindByAttribute) that can make this search more efficient.
Secondly, you can use IsJustAdded() and AreChildrenModified() on WMEs on the output-link (and below) to determine what has changed since the last time Soar was run.
Third, you can call AddOutputHandler() to register a function that is called whenever a specific attribute is added to the output link.
Finally, if that is not sufficient, it is possible to call GetNumberOutputLinkChanges() and GetOutputLinkChange() to get a list of all of the WMEs to have been added or removed since the last time Soar was run.
From this collection of methods it should be possible to support just about any manner of output model you wish to adopt, but we would recommend the "Command" model shown above.
The Command Line
Code:
// Create an example Soar command line std::string cmd = "excise --all" ; // Execute the command char const* pResult = pKernel->ExecuteCommandLine(cmd.c_str(),pAgent->GetAgentName()) ;
ExecuteCommandLineXML() is also available which returns the output as a structured XML message, making it easier and safer for a client to parse values from the output. See the online documentation for details the format of that XML output for each command.
Capturing print output
To capture output during a run you need to register for the smlEVENT_PRINT event which will be called periodically during the course of the run. To do this you need to define a handler function which will be called during the run. Here's a simple example:
Code:
void MyPrintEventHandler(smlPrintEventId id, void* pUserData, Agent* pAgent, char const* pMessage) { // In this case the user data is a string we're building up std::string* pTrace = (std::string*)pUserData ; (*pTrace) += pMessage ; }
Code:
std::string trace ; int callbackp = pAgent->RegisterForPrintEvent(smlEVENT_PRINT, MyPrintEventHandler, &trace) ;
Code:
result = pAgent->Run(4) ;
There is now also another way to get this output by registering for smlEVENT_XML_TRACE_OUTPUT. This event sends XML objects rather than text strings. Displaying these to the user requires more work by the client, but if the client wishes to parse the text working with XML is much easier. This is the approach taken in the Java debugger.
Events
There are a lot of events you can register for and the list given here will surely grow over time. Here are the types of the handlers in C++ and Java (the Java ones are a little different from the C++ and are more error prone as they're checked at runtime not at compile time):
C++ event handlers (if you're not sure how to convert these types into functions look at the example of the print handler in the previous section):
Code:
// Handler for Run events. // Passed back the event ID, the agent and the phase together with whatever user data we registered with the client typedef void (*RunEventHandler)(smlRunEventId id, void* pUserData, Agent* pAgent, smlPhase phase); // Handler for Agent events (such as creation/destruction etc.). typedef void (*AgentEventHandler)(smlAgentEventId id, void* pUserData, Agent* pAgent) ; // Handler for Print events. typedef void (*PrintEventHandler)(smlPrintEventId id, void* pUserData, Agent* pAgent, char const* pMessage) ; // Handler for Production manager events. typedef void (*ProductionEventHandler)(smlProductionEventId id, void* pUserData, Agent* pAgent, char const* pProdName, char const* pInstantion) ; // Handler for System events. typedef void (*SystemEventHandler)(smlSystemEventId id, void* pUserData, Kernel* pKernel) ; // Handler for XML events. The data for the event is passed back in pXML. // NOTE: To keep a copy of the ClientXML* you are passed use ClientXML* pMyXML = new ClientXML(pXML) to create // a copy of the object. This is very efficient and just adds a reference to the underlying XML message object. // You need to delete ClientXML objects you create and you should not delete the pXML object you are passed. typedef void (*XMLEventHandler)(smlXMLEventId id, void* pUserData, Agent* pAgent, ClientXML* pXML) ; // Handler for RHS (right hand side) function firings // pFunctionName and pArgument define the RHS function being called (the client may parse pArgument to extract other values) // The return value is a string which allows the RHS function to create a symbol: e.g. ^att (exec plus 2 2) producting ^att 4 // NOTE: This is the one place in clientSML where we use a std::string in an interface. If you wish to compile with a pure "C" interface // this can be replaced by a handler that is passed a buffer and a length. The length is passed within the framework already (from the kernel to here) // so this is an easy transition. typedef std::string (*RhsEventHandler)(smlRhsEventId id, void* pUserData, Agent* pAgent, char const* pFunctionName, char const* pArgument) ;
From Kernel:
Code:
public interface SystemEventInterface { public void systemEventHandler(int eventID, Object data, Kernel kernel) ; } public interface UpdateEventInterface { public void updateEventHandler(int eventID, Object data, Kernel kernel, int runFlags) ; } public interface StringEventInterface { public void stringEventHandler(int eventID, Object userData, Kernel kernel, String callbackData) ; } public interface AgentEventInterface { public void agentEventHandler(int eventID, Object data, String agentName) ; } public interface RhsFunctionInterface { public String rhsFunctionHandler(int eventID, Object data, String agentName, String functionName, String argument) ; } public interface ClientMessageInterface { public String clientMessageHandler(int eventID, Object data, String agentName, String functionName, String argument) ;
Code:
public interface RunEventInterface { public void runEventHandler(int eventID, Object data, Agent agent, int phase) ; } public interface ProductionEventInterface { public void productionEventHandler(int eventID, Object data, Agent agent, String prodName, String instantiation) ; } public interface PrintEventInterface { public void printEventHandler(int eventID, Object data, Agent agent, String message) ; } public interface xmlEventInterface { public void xmlEventHandler(int eventID, Object data, Agent agent, ClientXML xml) ; } public interface OutputEventInterface { public void outputEventHandler(Object data, String agentName, String attributeName, WMElement pWmeAdded) ; } public interface OutputNotificationInterface { public void outputNotificationHandler(Object data, Agent agent) ; }
Code:
public void runEventHandler(int eventID, Object data, Agent agent, int phase) { System.out.println("Received run event in Java") ; } // We pass back the agent's name because the Java Agent object may not yet // exist for this agent yet. The underlying C++ object *will* exist by the // time this method is called. So instead we look up the Agent object // from the kernel with GetAgent(). public void agentEventHandler(int eventID, Object data, String agentName) { System.out.println("Received agent event in Java") ; } public void productionEventHandler(int eventID, Object data, Agent agent, String prodName, String instantiation) { System.out.println("Received production event in Java") ; } public void systemEventHandler(int eventID, Object data, Kernel kernel) { System.out.println("Received system event in Java") ; } public void printEventHandler(int eventID, Object data, Agent agent, String message) { System.out.println("Received print event in Java: " + message) ; } public void xmlEventHandler(int eventID, Object data, Agent agent, ClientXML xml) { String xmlText = xml.GenerateXMLString(true) ; System.out.println("Received xml trace event in Java: " + xmlText) ; String allChildren = "" ; if (xml.GetNumberChildren() > 0) { ClientXML child = new ClientXML() ; xml.GetChild(child, 0) ; String childText = child.GenerateXMLString(true) ; allChildren += childText ; child.delete() ; } } public String testRhsHandler(int eventID, Object data, String agentName, String functionName, String argument) { System.out.println("Received rhs function event in Java for function: " + functionName + "(" + argument + ")") ; return "My rhs result " + argument ; }
Events in Tcl
Tcl requires that callbacks to a Tcl interpreter happen in the same thread as the interpreter is executing in. By default, this will not always happen for SML events. This is because there is an event thread started up by SML (running in the client) which is used to check for incoming events and make the necessary callback calls.
Therefore for Tcl we recommend shutting down this event thread and polling explicitly for incoming events. This can be done in a few lines of code like this:
Code:
# We want to make sure to handle events in the Tcl thread # so we turn off the event thread and poll for events instead. $_kernel StopEventThread checkForEvents $_kernel # Explicitly check for incoming commands and events every n milliseconds proc checkForEvents {k} { $k CheckForIncomingCommands after 10 checkForEvents $k }
Building an Environment
If you are converting an existing SGIO environment you should also read the "Moving from SGIO to SML" document as well as this section.
The Java implementation of Towers of Hanoi (TOH) is a useful reference, showing a simple environment environment. I'll use that to as an example here, so the code snippets will be in Java, but a very similar approach can be taken in other languages (currently Tcl or C++).
Initialization
The first step is to create an instance of the kernel and then create an agent. The name passed to CreateKernelInNewThread is the name of the library to load (DLL on Windows). This is optional in 8.6.2 and defaults to SoarKernelSML.
It is important to check for an error using the kernel.HadError() method. CreateKernel will not return an empty kernel object even if initialization fails. This is deliberate design to ensure that meaningful errors can be reported to the user.
Initialization Code Sample
Code:
// create our Soar kernel kernel = Kernel.CreateKernelInNewThread(); if (kernel.HadError()) { // Better to use a Message Box to display this if your platform/toolkit allows. System.out.println("Error creating kernel: " + kernel.GetLastErrorDescription()) ; System.exit(1); } agent = kernel.CreateAgent(AGENT_NAME); boolean load = agent.LoadProductions("towers-of-hanoi-SML.soar"); if (!load || agent.HadError()) { throw new IllegalStateException("Error loading productions: " + agent.GetLastErrorDescription()); }
Mapping from the environment's state to input values involves calls to create, update and destroy working memory elements using calls like these:
Code:
agent.Create<type>WME (e.g. agent.CreateIntWME) agent.Update() agent.DestroyWME
A key step after making multiple calls to modify the input link is to call Commit(). All changes to working memory are buffered (in the environment) until the moment Commit is called, at which point they are all sent over as a single large message to the kernel. This greatly improves performance, but you must remember to call Commit before running the agent or your changes won't actually appear on the agent's input link. This was changed in 8.6.2 so the default now is that Commit() is called automatically each time a wme is modified on the input link. To enable the faster version which then requires manual calls to Commit() call SetAutoCommit(false). Calling Commit multiple times during a single input cycle will not cause problems.
Output
The most common way to examine output from an agent is to use the "Commands" model. With this method the agent places each output command on the output-link using the format:
Code:
(X ^output-link I3) (I3 ^command-name C1) (C1 ^param1 value ^param2 value)
If the agent adopts this format for output commands they can be easily retrieved by the environment using code like this:
Update World Code Sample -- get Output, Change-World-State, then send Input
Code:
private void updateWorld() // See if any commands were generated on the output link // (In general we might want to update the world when the agent // takes no action in which case some code would be outside this if statement // but for this environment that's not necessary). if (agent.Commands()) { // perform the command on the output link Identifier command = agent.GetCommand(0); if (!command.GetCommandName().equals(MOVE_DISK)) { throw new IllegalStateException("Unknown Command: " + command.GetCommandName()); } if (command.GetParameterValue(SOURCE_PEG) == null || command.GetParameterValue(DESTINATION_PEG) == null) { throw new IllegalStateException("Parameter(s) missing for Command " + MOVE_DISK); } int srcPeg = command.GetParameterValue("source-peg").charAt(0) - 'A'; int dstPeg = command.GetParameterValue("destination-peg").charAt(0) - 'A'; // Change the state of the world and generate new input moveDisk(srcPeg, dstPeg); // Tell the agent that this command has executed in the environment. command.AddStatusComplete(); // Send the new input link changes to the agent agent.Commit(); // "agent.GetCommand(n)" is based on watching for changes to the output link // so before we do a run we clear the last set of changes. // (You can always read the output link directly and not use the // commands model to determine what has changed between runs). agent.ClearOutputLinkChanges() ; if (isAtGoalState()) fireAtGoalState(); } }
The method AddStatusComplete adds (C1 ^status complete) to the command structure, indicating to the agent that this command has executed and can now be removed by the agent.
(Note that this is simple form of input that in this case is sent to the output link rather than the input link. This is technically a bit of "back door" input, but is simpler than passing a large structure to the input link and having the agent match up the two. This addition is made during the agent's next input cycle as with all input).
In order for the Commands method to work, the system has to keep track of changes to working memory as only newly added commands are reported through the GetCommand method. For this process to work, the environment must call ClearOutputLinkChanges() before running the agent again.
While this Command model should be sufficient for almost all environments, if there is a need to process other structures, the environment can always choose to ignore this model partially or completely. First, the method GetCommand returns an Identifier object. The environment can use the methods GetNumberChildren and GetChild to walk this Identifier and locate any arbitrary working memory element beneath an Identifier or the environment can use the FindByAttribute method to retrieve the values of working memory elements that have a particular attribute.
In fact a environment can abandon the Command model completely and simply call GetOutputLink() to get the Identifier object at the top of the output link and proceed to examine the tree (graph) from there.
Running the agent(s)
There are two main methods for running an agent:
- RunSelfForever() and it's twin RunAllAgentsForever()
- RunSelf(1) and RunAllAgents(1) (to step the environment rather than running it).
Let's assume we have the updateWorld() method from above, which should have the form:
Code:
void updateWorld() { check-for-output() ; update-world-state() ; send-new-input() ; }
Run Sample Code
Code:
public void run() { m_StopNow = false ; // Start a run // (since this is single agent could use agent.RunSelfForever() instead, but this shows how to run multi-agent environments) kernel.RunAllAgentsForever() ; } public void step() { // Run one decision kernel.RunAllAgents(1) ; } public void stop() { // We'd like to call StopSoar() directly from here but we're in a different // thread and right now this waits patiently for the runForever call to finish // before it executes...not really the right behavior. So instead we use a flag and // issue StopSoar() in a callback. m_StopNow = true ; }
Code:
public void registerForUpdateWorldEvent() { int updateCallback = kernel.RegisterForUpdateEvent(sml.smlUpdateEventId.smlEVENT_AFTER_ALL_OUTPUT_PHASES, this, "updateEventHandler", null) ; } public void updateEventHandler(int eventID, Object data, Kernel kernel, int runFlags) { // Might not call updateWorld() depending on runFlags in a fuller environment. // See the section below for more on this. updateWorld() ; // We have a problem at the moment with calling Stop() from arbitrary threads // so for now we'll make sure to call it within an event callback. // Do this test after calling updateWorld() so that method can set m_StopNow if it // wishes and trigger an immediate stop. if (m_StopNow) { m_StopNow = false ; kernel.StopAllAgents() ; } }
- RunAllAgentsForever() is currently (8.6.2) a blocking call, so we usually spawn a thread and issue this call in that thread.
- StopAllAgents() can't currently be called from an arbitrary thread (or it blocks waiting for the Run to terminate). This may be fixed in a later version, but for now calling it from an event callback solves this problem.
Code:
while (!stopped) { run(1) ; update-world() ; }
Also in Tcl please be sure to read about how to poll for events in Tcl (see Section 1.6.1).
Supporting running without updating the environment
For some environments it may make sense to allow the user to run an agent without starting the environment. This can be helpful when debugging one agent or having reached a particular situation in the world, stopping and wanting to step slowly to observe the reasoning.
To support this the environment should check the runFlags parameter passed to the updateEventHandler. This is a bit field which currently consists of the values in this enum:
Run Flags
Code:
typedef enum { sml_NONE = 0, // No special flags set sml_RUN_SELF = 1 << 0, // User included --self flag when running agent sml_RUN_ALL = 1 << 1, // User ran all agents sml_UPDATE_WORLD = 1 << 2, // User explicitly requested world to update sml_DONT_UPDATE_WORLD = 1 << 3, // User explicitly requested world to not update } smlRunFlags ;
if sml_RUN_SELF set and SML_UPDATE_WORLD not set or sml_DONT_UPDATE_WORLD set then don't call updateWorld().
The meaning of this would be that:
- run --self would cause the agent to run but not the environment
- run --noupdate would cause all agents to run but not the environment
- run --self --update would cause one agent to run and update the environment
- run would cause all agents to run and update the environment.
Integrating with other clients (esp. the debugger)
One final issue is updating the controls in the environment correctly if the user runs Soar from the debugger and not the environment (e.g. disabling the run button and enabling stop during a run).
There are two approaches to this. First, ignore the problem and don't enable/disable controls. Instead, just handle errors if the user tries to issue a second run while a run is already going ahead. The second option is to register for the smlEVENT_SYSTEM_START and smlEVENT_SYSTEM_STOP events and use these to enable/disable the UI in the environment in appropriate ways.
Here's an example for implementing this support:
Registering and Handling Start/Stop Sample Code
Code:
public void registerForStartStopEvents() { if (kernel != null) { int startCallback = kernel.RegisterForSystemEvent(smlSystemEventId.smlEVENT_SYSTEM_START, this, systemEventHandler, null) ; int stopCallback = kernel.RegisterForSystemEvent(smlSystemEventId.smlEVENT_SYSTEM_STOP, this, systemEventHandler, null) ; } } public void systemEventHandler(int eventID, Object data, Kernel kernel) { if (eventID == sml.smlSystemEventId.smlEVENT_SYSTEM_START.swigValue()) { // The callback comes in on Soar's thread and we have to update the buttons // on the UI thread, so switch threads. dpy.asyncExec(new Runnable() { public void run() { updateButtons(true) ; } } ) ; } if (eventID == sml.smlSystemEventId.smlEVENT_SYSTEM_STOP.swigValue()) { // The callback comes in on Soar's thread and we have to update the buttons // on the UI thread, so switch threads. dpy.asyncExec(new Runnable() { public void run() { updateButtons(false) ; } } ) ; } } public void updateButtons(boolean running) { boolean done = game.isAtGoalState() ; startButton.setEnabled(!running && !done) ; stopButton.setEnabled(running) ; resetButton.setEnabled(!running && !done) ; stepButton.setEnabled(!running && !done) ; }
To learn more about ClientSML and SML in general, the best documentation is the header files for the methods in ClientSML. In particular, sml_ClientKernel.h, sml_ClientAgent.h and sml_ClientIdentifier.h contain a lot of useful methods and explanations.
Beyond that check the Soar XML Interface Spec and any other documentation in its vicinity.
Using other languages
The Tcl and Java interfaces were generated by SWIG (http://www.swig.org). We have provided some custom code to help SWIG out in a few places, mostly with callbacks. If you're interested in creating interfaces for other languages that SWIG supports, check out the SWIG documentation and try to follow our existing solutions.
A Tcl package, called Tcl_sml_ClientInterface is available. On Windows it is located in the soar-library directory. This can be used by including the following line in your Tcl code:
Code:
package require tcl_sml_clientinterface
Example Environments in Java
Here are two example clients written in Java, intended to explain the basics of using SML in that language. Thorough explanatory comments are inlined.
- A very simple Soar environment.
- A more complex interactive asynchronous environment.
Memory Management
Memory management is actually really easy. Generally, the only objects you should explicitly delete are the kernel object and any objects you directly allocated through a call to new. In Java and Tcl, this generally means you can just let things go out of scope when you're done with them. There are a couple special cases you should be aware of, though:
- Agent objects are automatically deleted when the owning Kernel object is deleted (actually, when the call to Kernel::Shutdown is made, which you should always make before deleting the kernel). If you want to destroy an agent earlier, you can by making a call to Kernel:
estroyAgent. Under no circumstances should you delete (in the C++ sense) an Agent object.
- In Java if you create a ClientXML object through xml = new ClientXML() you should call xml.delete() on it when you're done. This isn't strictly required (the garbage collector will get it eventually) but is good practice and will avoid messages about leaked memory when the application shuts down. As per the general rule, in C++ if you create it with new you're responsible for destroying it with delete.
- Since there can be multiple clients interacting with the same kernel and agents, your application needs to be listening for the appropriate events so if some other client deletes/destroys a kernel or agent your application is using, you don't crash. Specifically, listen for the BEFORE_AGENT_DESTOYED and BEFORE_SHUTDOWN events so you can clean things up as needed in your application.
It is often desirable to maximize the performance of your SML application. This section assumes that you just want to make things as fast as possible after you have finished debugging your application. Debugging is an inherently slow process, so these tips will be less helpful while you're still debugging.
- Compile with optimizations turned on. In Visual Studio this means doing a release build. On Linux and OS X, the default settings are probably sufficient, but you can experiment with new settings if you want (let us know if you find better settings).
- Put primary application and Soar in the same process. That is, use CreateInNewThread or CreateInCurrentThread, not CreateRemoteConnection. Using a remote kernel means socket communications are used, which is slow.
- Don't register for unnecessary events. Every event that is registered for causes extra work to be done. Try to find an appropriate event to register for so you don't end up getting more event calls than you actually need -- that is, try to avoid registering for events which occur more frequently than you need and then filtering them on the application side.
- Don't connect the debugger. Connecting the debugger creates a remote connection and also registers for several events.
- Set watch level 0. Even if you don't have a client registered for any of the print or XML events, work is still done internally to generate some of the information that would have been sent out. Setting watch level 0 avoids this work.
- Disable monitor productions. Again, even if no client is registered to print out the text of monitor productions, work is still done internally to prepare the text. Monitor productions can be disabled by excising them or commenting them out, but an easier method is to have each monitor production test a debug flag in working memory which is set by some initialization production or operator. Thus all of the monitor productions can be turned on or off by changing one line of code.
- Disable timers. Soar uses timers internally to generate the output of the stats command. If you don't need this information, you can use the timers --off command to disable this bookkeeping. This can make a significant difference in the watch 0 case.
- Avoid running agents separately. Instead of calling RunSelf or RunSelfTilOutput on each agent, just call RunAllAgents on the kernel itself. This runs all agents together and avoids the overhead of running them separately. The absolute best you can do is to call RunAllAgentsForever as described in section 2.4 -- this avoids repeatedly calling the run functions at all and will make it easier to stop and restart your application from the debugger (or other clients).
- In the case where the absolute best performance under SML is desired, use CreateKernelInCurrentThread instead of CreateKernelInNewThread and set the "optimized" flag to true in the parameters passed to CreateKernelInCurrentThread. This means Soar will execute in the same thread as your application. Without this each call to and from the Soar kernel requires a context switch (assuming a single processor machine). This method also eliminates the thread which polls for new events. This means you must poll for the events yourself by periodically calling CheckForIncommingCommands, which is a little more work for the programmer.