Das Skriptsystem wird initialisiert, indem eine oder mehrere Instanzen der Klasse „Manager“ erstellt werden. Jede Instanz repräsentiert eine Skriptumgebung, in der Skripte geladen und ausgeführt werden können. Jede Instanz stellt außerdem einen globalen Variablenbereich bereit, über den die Skripte Daten austauschen können.
Manager manager = new Manager();
Sobald ein Manager verfügbar ist, können Skripte durch Erstellen von Instanzen der Klasse „Script“ geladen werden. Der Konstruktor des Skriptobjekts benötigt eine Referenz auf den Skriptmanager und einen Namen zur Identifizierung des Skripts. Standardmäßig entspricht dieser Name einem Dateinamen auf der Festplatte.
Script script = new Script(manager, "script.txt");
Der Konstruktor des Skriptobjekts kompiliert den Skriptquellcode automatisch in Scriptstack-Bytecode. Standardmäßig wird der generierte Bytecode mithilfe eines „Peephole“ Assembleroptimierers optimiert. Debug Anweisungen werden hinzugefügt, um die Zuordnung des generierten Codes zum Originalquellcode zu erleichtern. Diese Einstellungen können über die Methoden „Debug“ und „Optimise“ des „Manager“ gesteuert werden.
Ein Skriptobjekt repräsentiert lediglich Programmanweisungen (sog Token), nicht aber seinen Ausführungszustand. Um das Skript auszuführen, muss eine Instanz der Klasse „Interpreter“ erstellt werden. Der Konstruktor dieser Klasse benötigt eine Referenz auf das auszuführende Skript oder eine Referenz auf eine seiner Funktionen. Eine Skriptreferenz bewirkt die Ausführung der „main“-Funktion. Der Skriptkontext ermöglicht die Ausführungssteuerung und den Zugriff auf den Ausführungszustand, einschließlich der während der Ausführung definierten Variablen, der nächsten auszuführenden Anweisung usw. Die Klasse „Interpreter“ repräsentiert eine laufende Instanz eines Skripts. Daher können mehrere Instanzen desselben Skriptobjekts innerhalb desselben Skriptmanagers ausgeführt werden, indem mehrere Interpreter erstellt werden, die auf dasselbe Skript verweisen.
// create a context for the script's main function Interpreter interpreter = new Interpreter(script); // also creates a context for the script's main function Interpreter interpreter = new Interpreter(script.MainFunction); // create a context for one of the script's named functions Function scriptFunction = script.Functions["Just looking..."]; Interpreter interpreter = new Interpreter(scriptFunction);
Das Interpreter-Objekt ermöglicht die Ausführung des referenzierten Skripts über drei Varianten seiner „Interpret()“-Methode: unbegrenzte Ausführung, Ausführung innerhalb eines bestimmten Zeitintervalls oder Ausführung bis zu einer maximalen Anzahl ausgeführter Anweisungen. Die erste Variante erlaubt die unbegrenzte Ausführung der referenzierten Skriptfunktion oder deren Beendigung. Enthält das Skript eine Endlosschleife, blockiert diese Methode unbegrenzt, sofern keine Unterbrechung ausgelöst wird. Die „Interpret()“-Methode gibt die Gesamtzahl der seit ihrem Aufruf ausgeführten Anweisungen zurück.
// execute indefinitely, or until termination, or a script interrupt is generated interpreter.Interpret();
Die zweite Variante der „Interpret()“-Methode ermöglicht es dem Interpreter, bis zu einer vorgegebenen maximalen Anzahl von Anweisungen auszuführen. Der Interpreter kann die Ausführung abbrechen, bevor das Maximum erreicht ist, wenn keine weiteren Anweisungen zu verarbeiten sind oder eine Unterbrechung ausgelöst wird.
// execute up to a maximumum of 10 statements interpreter.Interpret(10);
Die dritte Variante der „Interpret()“-Methode akzeptiert einen TimeSpan, der das maximal zulässige Zeitintervall für die Skriptausführung definiert. Die Methode kann die Ausführung vor Ablauf des angegebenen Intervalls abbrechen, wenn keine weiteren Anweisungen zu verarbeiten sind oder eine Unterbrechung ausgelöst wird. Bei einem Skript mit einer ausgewogenen Anzahl verschiedener Anweisungen kann diese Methode beispielsweise verwendet werden, um die Geschwindigkeit des Skriptsystems in der Zielumgebung in Bezug auf die pro Sekunde ausgeführten Anweisungen zu ermitteln.
// execute for up to 10 milliseconds TimeSpan tsInterval = new TimeSpan(0, 0, 0, 0, 10); interpreter.Interpret(tsInterval);
Ein Interpreter führt seine referenzierte Skriptfunktion normalerweise unbegrenzt für ein bestimmtes Zeitintervall aus, bis eine maximale Anzahl von Anweisungen ausgeführt wurde oder keine weiteren Anweisungen mehr zu verarbeiten sind. In manchen Fällen ist es jedoch wünschenswert, die Ausführung vorzeitig abzubrechen, beispielsweise um die Kontrolle an den Host zurückzugeben, sobald bestimmte Anweisungen ausgeführt wurden, oder weil ein Skript zu rechenintensiv ist, um es in einem Durchgang auszuführen.
Conscript provides two ways for generating script interrupts:
Um das Laden von Skripten aus anderen Quellen wie Archivdateien, dem Netzwerk oder Datenbanken zu ermöglichen, kann ein benutzerdefinierter Scanner an den Manager gebunden werden. Dieser Scanner kann jede Klasse sein, die das Interface `Scanner` implementiert.
// custom script loader class public class MyScanner : Scanner { public List<string /> Scan(String strResourceName) { // loader implementation here... } } // in initialisation code... MyScanner scanner = new MyScanner(); interpreter.scanner = scanner;
Eine Möglichkeit, einem Skript die Kommunikation mit der Hostanwendung oder deren Steuerung zu ermöglichen, besteht darin, dass die Anwendung die lokalen Variablen des zugehörigen Interpreters und die globalen Variablen des zugehörigen Managers abfragt. Dies geschieht durch Abfragen der „LocalMemory“-Eigenschaft des „Interpreter“-Objekts, der „ScriptMemory“-Eigenschaft des „Script“-Objekts oder der „GlobalMemory“-Eigenschaft des „Manager“-Objekts.
// get value of local variable int i = (int)interpreter.LocalMemory["variableName"]; // get value of script variable int i = (int)script.ScriptMemory["variableName"]; // get value of global variable int i = (int)manager.SharedMemory["variableName"];
A more powerful alternative to allow a script to interface with the host application is to register host functions with the script manager and assign a script handler at script context or manager level. The script handler in turn provides an implementation for the host functions. These functions are first defined by creating an instance of the HostFunctionPrototype class to define the functions' names, parameters and return values. Unlike script-defined functions, host functions can enforce type-checking on parameters and return values that are performed at compile time. Thus, scripts that use host functions must be loaded within a script manager that has the required host functions registered beforehand.
// define route Routine routine = new Routine((Type)null, "Print", (Type)null, "A function to print text");
A number of constructors are provided for quick construction of function prototypes with up to three parameters. For functions requiring more parameters, a constructor that accepts a type list, List<Type>, is also provided:
// prepare parameter type list List<Type> listParameterTypes = new List<Type>(); listParameterTypes.Add(typeof(string)); listParameterTypes.Add(typeof(int)); listParameterTypes.Add(typeof(int)); listParameterTypes.Add(typeof(bool)); listParameterTypes.Add(typeof(ArrayList)); // define routine with many parameters Routine routine = new Routine((Type)null, "Print", listParameterTypes);
Once the host function prototype is defined, it can be registered with the script manager. The function prototype ensures that a corresponding host function is recognised during compilation and runtime and that the parameters passed alongside correspond in number, type and order to those defined in the function prototype. The following statement registers a host function prototype without a handler:
// register host function manager.Register(routine);
The registration method illustrated above requires handler objects to be attached to every ScriptContext created from a script that uses the host function. A handler may be any class that implements the HostFunctionHandler interface consisting of the method OnHostFunctionCall(…). A good approach is to extend the class that owns the relevant script context. This is illustrated by the following example, which implements host functions for enemy A.I. scripts in a game:
public class Program : Host { private Interpreter interpreter; public Main(script script) { interpreter = new Interpreter(script); interpreter.Handler = this; } public object Invoke(String functionName, List<object> parameters) { if (functionName == "Print") { string str = (int)parameters[0]; Console.WriteLine(str); return; } return null; } }
Worth noting in the above example is that no parameter validation is performed, nor is it needed because the validation is performed at compile time. The ability to define a different handler for every script context allows a host function to have different implementations and / or contexts depending on the script contexts to which respective script handlers are bound. For example, the implementation for a Move(x, y) function within a script might affect the movement of a player character, a non-player character or a projectile. Alternatively, a host function prototype may be registered with an associated handler directly:
// register global Sine function Routine routine = new Routine(typeof(float), "Print", typeof(float)); manager.Register(routine, printHandler); // register global Cosine function routine = new Routine(typeof(float), "Read", typeof(float)); manager.Register(routine, readHandler);
This approach allows the association of a common handler that is shared amongst all script contexts created within the same script manager. A typical example is the registration of trigonometric and other common math functions required in many scripts.
Registering shared host functions individually required a substantial amount of setup code. To alleviate this problem, a HostModule interface is provided to allow the registration of multiple host functions in bulk. The implementation of a host module entails defining a method that returns a list of function prototypes implemented by the module, together with a handler method as per the HostFunctionHandler interface. This provides an implementation for the functions. The following illustrates an alternative implementation of the earlier trigonometry functions example:
public class TrigonometryModule : HostModule { // list of functions stored statically for one-off creation private static ReadOnlyCollection<hostfunctionprototype /> s_listHostFunctionPrototypes; // module constructor public TrigonometryModule() { // if list created, don't do anything else if (s_listHostFunctionPrototypes != null) return; // create list of function prototypes List<hostfunctionprototype /> listHostFunctionPrototypes = new List<hostfunctionprototype />(); HostFunctionPrototype hostFunctionPrototype = null; // add Sine prototype to list hostFunctionPrototype = new HostFunctionPrototype(typeof(float), "Sin", typeof(float)); listHostFunctionPrototypes.Add(hostFunctionPrototype); // add Cosine prototype to list hostFunctionPrototype = new HostFunctionPrototype(typeof(float), "Cos", typeof(float)); listHostFunctionPrototypes.Add(hostFunctionPrototype); // add Tangent prototype to list hostFunctionPrototype = new HostFunctionPrototype(typeof(float), "Tan", typeof(float)); listHostFunctionPrototypes.Add(hostFunctionPrototype); s_listHostFunctionPrototypes = listHostFunctionPrototypes.AsReadOnly(); } // returns list of available functions public ReadOnlyCollection<hostfunctionprototype /> HostFunctionPrototypes { get { return s_listHostFunctionPrototypes; } } // implements functions public object OnHostFunctionCall(String strFunctionName, List<object> listParameters) { if (strFunctionName == "Sin") // implement Sine return (float)Math.Sin((float)listParameters[0]); else if (strFunctionName == "Cos") // implement Cosine return (float)Math.Cos((float)listParameters[0]); else if (strFunctionName == "Tan") // implement Tangent return (float)Math.Tan((float)listParameters[0]); // unknown function (should not happen) throw new ExecutionException( "Unimplemented function '" + strFunctionName + "'."); } }
Host module registration is very similar to individual function registration:
// create module instance TrigonometryModule trigonometryModule = new TrigonometryModule(); // register module m_scriptManager.RegisterHostModule(trigonometryModule)