Custom Language Wrappers - GimpArm/AntRunner GitHub Wiki
Ant Runner is written in C# .Net and initially intended for Windows .Net developers. To allow other developers to compete a loader and wrapper model was created to act as a bridge. More Loaders and Wrappers can be added for other languages.
Ants in other languages do have a slight disadvantage due to the overhead of serialization and communication. Although it is extremely small, in the neighborhood of ~10ms less processing time. In a properly written ant this should not be a problem. The objects needed for communication are small and you should take care to serialize/deserialize them in the most efficient way possible in .Net and the target language.
Look to the already implemented examples in the source code.
An interface to implement that provides information on the file extension to load and generates information needed to create a new instance of the ant wrapper.
public interface AntRunner.Interface.IWrapperLoader
Type | Name | Summary |
---|---|---|
String |
Extension | Contains a string of the extension this wrapper can load. |
public string Extension => "js";
Type | Name | Summary |
---|---|---|
AssemblyLoaderData |
MakeLoaderData(String filename) |
Takes in the pull path to the ant file to be loaded and returns AssemblyLoaderData containing information on how to crate a new instance of the Loader. |
public AssemblyLoaderData MakeLoaderData(string filename)
{
return new AssemblyLoaderData
{
AssemblyName = Assembly.GetExecutingAssembly().GetName(),
TypeString = "AntRunner.Wrapper.Js.JsAnt",
ConstructorParameters = new object[] { filename }
};
}
Object containing the information needed to load this file type.
public class AntRunner.Interface.AssemblyLoaderData
Type | Name | Summary |
---|---|---|
String |
TypeString | String of the full name of the ant class. |
AssemblyName |
AssemblyName | AssemblyName information of the assembly containing the ant class. |
Object[] |
ConstructorParameters | Object array of the parameters needed for the ant constructor. Leave null for no parameters. |
Wrappers are defined in the AntRunner.exe.config
file. Add it to <Wrappers>
section with a path
that is relative to the Wrappers
folder and the type
which is full type name of the class which implements IWrapperLoader
.
<wrapperLoader>
<wrappers>
<add path="php\AntRunner.Wrapper.Php.dll" type="AntRunner.Wrapper.Php.Loader" />
<add path="js\AntRunner.Wrapper.Js.dll" type="AntRunner.Wrapper.Js.Loader" />
<add path="python\AntRunner.Wrapper.Python.dll" type="AntRunner.Wrapper.Python.Loader" />
<add path="ruby\AntRunner.Wrapper.Ruby.dll" type="AntRunner.Wrapper.Ruby.Loader" />
</wrappers>
</wrapperLoader>
This is just a class inheriting from AntRunner.Interface.Ant
which initializes and manages the other language. You can (and probably should) implement IDisposable
to clean up any processes which may be left orphaned.
Many scripting languages are intended to start and stop but this poses a problem of saving and restoring state information. Doing so would cause even more overhead and put the ant at a very large disadvantage, not to mention the added complexity. Therefore it is recommended to keep the environment running in a loop and wait for input. There are some languages with a server environment that can be started and communicated with, like Node.js, but most do not. I have had success by creating a process and controlling it via STDIN and STDOUT in a wrapper script.
You should package all required binaries with the wrapper. This includes reference libraries for .Net. The only exceptions are AntRunner.Interface and Newtonsoft.Json which are automatically loaded and should not be included. Some languages like Python and Ruby contain extremely large runtime environments. These should be included but can be compressed and decompressed when run. Although it is possible to require a configuration file with the ant that contains information about the runtime environment that is installed by the user.
The constructor should initialize the target environment and keep it running. If there is an error it should throw an exception and clean up after itself. This can take as long as needed and it is better to initialize everything required here as to not cause slowdowns while the ant is running.
After an ant is loaded the first piece of information required is the name. Since Name is a property, this can be requested from the environment on demand.
The flag needs to be a stream. It is recommended to read the file from the disk and return a MemoryStream
to prevent locking which may prevent an ant from loading multiple times.
Initialize is called once at the start of the game. The information is basic, 5 integer values, so a simple json array serialization is recommended. No result is needed so simply sending the command is enough.
var initSerialized = $"[{mapWidth},{mapHeight},{(int)antColor},{startX},{startY}]";
The tick is run every turn and is sensitive to time constraints. It is also responsible For communicating the AntAction
and setting it in the wrapper. In this case it is recommended to serialize the GameState
as quickly as possible, send it to the environment and wait for a result of the AntAction
which can be set to this.Action
. The wrapper environment should also be responsible for resetting the Action
to AntAction.Wait
or 0
in the target environment. This will prevent a process that takes too long from accidentally moving the ant.
var responseSerialized = state.Response == null ? "null" : $"{{\"Distance\":{state.Response.Distance},\"Item\":{(int)state.Response.Item}}}";
var stateSerialized = $"{{\"TickNumber\":{state.TickNumber},\"Response\":{responseSerialized },\"Event\":{(int)state.Event},\"HasFlag\":{(state.HasFlag?"true":"false")},\"FlagX\":{state.FlagX},\"FlagY\":{state.FlagY},\"AntWithFlag\":{(int)state.AntWithFlag}}}";
Communicating with the target environment can be tricky. Some languages provide a server like Node.js but most do not. The communication protocol you decide on should be simple and quick. There is only a limited amount of data and commands needed so don't over engineer this.
I have settled on a very basic method of sending strings and using the first character to designate the command and all remaining characters to contain serialized data if needed.
Command | Meaning | Summary |
---|---|---|
T |
Tick | This is followed by a serialization of the GameState and expects an Int32 response of the AntAction . |
I |
Initialize | This is followed by a serialized array of the 5 integers required for the method arguments. No response is expected. |
N |
Name | Expects a String response of the ant's name. |
P |
Ping | A test command so we know the environment is running even if the ant contained in it is not properly functioning. Expects a string "Ping" as a response. |
X |
Exit | When received the environment should shut itself down. |
There isn't much data that needs to be serialized but this still should be done as quickly as possible since every millisecond lost to overhead is a millisecond not available to the ant. It is temping to just use the Newtonsoft.Json library and forget about it. This will probably work but it is not the most efficient method. You should think about what is the most efficient deserialization method of the target language and whether you can implement this reliably in .Net. If you do choose JSON then it should be done as a quick string interpolation as in the Initialize
and Tick
examples.
Take PHP for example. JSON is not the fastest serialization available. It is better to use PHP's native serialization which an array looks like a:5:{i:0;i:100;i:1;i:100;i:2;i:1;i:3;i:37;i:4;i:63;}
. This has the added benefit of communicating type information so objects can be deserialized directly as their defined class instead of a generic object. It is also easy to create in C# using basic string interpolation since the data being sent is simple and repetitive.
On the other hand, Python has a native serialization (Marshal) but this is a binary format and Python version dependent. This will be difficult for C# to implement and transmit. Python also has a Pick module but again this is a binary format and surprisingly is slower than standard JSON.