|
Context Scripting Suite is designed to simplify the
task of adding scripting functionality to applications. The package contains
four major classes, that work together to compile and execute scripts and to
resolve names of objects or properties, that may appear in scripts. These classes
are: TCtxCompiler, TCtxScriptCode, TCtxScript
and TCtxIntrospector.
TCtxCompiler is a virtual class, ancestor of every
compiler, that can produce code, executable by TCtxScript component. The package
includes default Pascal-like script compiler, implemented by TCtxPasCompiler.
Objects of this class register itself in CtxCompilers list and are not directly
called by user. They are located by Language name and invoked from inside the
script class.
TCtxScriptCode is a final class, representing a code,
executable by TCtxScript. It also contains source code, script name and, - after
compilation, - a list of local symbols (parameters and local variables), found
in the script. You should never need to inherit from this class. TCtxScriptCode
may represent both methods and expressions. All parameters and local variables
are considered Variants, so type declaration is not currently supported. Context
Script supports in, out and var parameters.
Examples of script methods (procedure & function):
procedure MyProc(A; var B; in C, D; out E);
var LocalVariable1, LocalVariable2;
begin
// E parameter is NULL, as out parameters are nullified.
LocalVariable1 := A + B * C + D - E;
LocalVariable2 := LocalVariable1 div 2;
// B and E parameters will be changed by reference.
// This is a way to return values from scripts.
B := LocalVariable2;
E := LocalVariable1;
end;
function MyFunc(x);
begin
Result := x * sin(x);
end;
Example of script expression:
MyForm.Left + (MyForm.Width div 2)
Each script, represented by TCtxScriptCode may have a scope,
which determines how the names within the code's context are resolved. This
scope could be:
Global |
in which case any name (that is not a local
variable or parameter), appeared in script, is treated as a field of GlobalScope
object (see GlobalScope property of TCtxScript) or as a field or method
of SystemScope object if it cannot be resolved on GlobalScope. The following
method should be used to execute Script on Global scope:
CtxScript.Execute(Script);
or
CtxScript.Execute(Script, Parameters);
|
Object |
in which case any name appeared in script
will be first treated as a field or method of object, then (if not resolved)
will be resolved on GlobalScope or SystemScope. The following method should
be used to execute Method: TCtxScriptCode as method of Instance:
CtxScript.Execute(Method, Instance);
|
Local |
in which case script cannot have its own
local variables or parameters, but is considered executed in the context
of another script. This means, that it can refer to local variable and parameters
of that script as well as object & global scope fields and methods. This
feature is used mostly to allow watch expressions, that can be executed
in debug time within the context of executing script. This following method
should be used to execute a local script:
CtxScript.Execute(Script, CodeScope);
|
A script code is only compiled once, before first execution.
Names in its context are also only resolved once, during the first execution.
This is done to avoid complexity of compile-time name resolution and to allow
flexible object models (see example with TDataSet object model below).
A simple example of using TCtxScriptCode objects to execute
script:
MyScript := TCtxScriptCode.Create;
try
MyScript.Code := 'function Sqr(x); begin Result := x*x; end;';
ShowMessage( '12 power 2 is ' + CtxScript1.Execute(MyScript, 12)
);
// Second time over the script is not compiled
ShowMessage( '16 power 2 is ' + CtxScript1.Execute(MyScript, 16)
);
finally
MyScript.Free;
end;
An instance of TCtxScriptCode can be stored and accessed
many times. Unless its Code changes, it will not be recompiled, so all the subsequent
executions will go faster.
TCtxScript is a processor or virtual machine, that
loads and executes scripts (TCtxScriptCode class). It is implemented as one
register (called Result) processor with a stack of Variant. This means, that
all variables in Context Scripting are currently represented by Variants. Context
Scripting works with all standard variant types and also adds two custom types:
varObject - represents a Delphi object; reference is stored in VPointer
field of TVarData record. In order to convert any Delphi object into variant
for passing it as a parameter to Context Script, use VarFromObject and VarToObject
functions, declared in CtxScript unit.
CtxScript1.Execute(Memo1.Text, VarFromObject(MainMenu));
varReference - represents a pointer to any value. This Variant type
is used to pass variables and object by reference. Use VarRef function declared
in CtxScript to create a Variant, referencing other Variant. For example,
to pass local variable by reference to a script (so that the script can change
it) one can use the following code:
var
A: Variant;
begin
CtxScript1.Execute(Memo1.Text, VarRef(A));
ShowMessage(A);
end;
CtxScript implements two methods of resolves field
names in scripts.
Variants, of type varDispatch, containing ole automation
objects, (i.e. providing IDispatch interface), are invoked via standard ole
automation procedure. This means, that one can instantiate an MS "Word Application"
object in Context Script, using code like this:
Word := CreateOleObject('Word.Application');
and then access its properties and methods as if it were
a regular Delphi object. Ole automation is used to perform this call:
Word.Selection.TypeText('It is really easy to use Ole Automation!');
Variants of type varObject as well as for any other
object (GlobalScope, SystemScope) CtxScript uses registered "introspectors",
descendant of virtual class TCtxIntrospector.
TCtxIntrospector is a base class for objects, implementing
IDispatch-like interface for different Delphi classes. Introspectors are only
used with Delphi classes, ancestors of TObject. Introspectors are registered
to Delphi class names and organized in chains. This way each class may have
several introspectors of different type, implementing each different object
model for instances of that class. There're four different types of introspectors
implemented:
TCtxObjectIntrospector
TCtxPersistentIntrospector
TCtxComponentIntrospector
TCtxDataSetIntrospector
TCtxInstanceIntrospector
and
TCtxCustomIntrospector
More introspectors can be implemented for other object models,
however this set is sufficient for resolving virtually any name within Delphi
scope. For example, if an object is a TComponent, the scripting engine can resolve
its child components by Name (this is done by TCtxComponentIntrospector). If
the name is not found, the engine will look for published properties (this is
done by TCtxPersistentIntrospector). If a published property is not found, the
engine will look for published object's fields (done by TCtxObjectIntrospector).
If the name is still not found, the engine will move on to object's parent class
and continue until TObject is reached. In addition to that TDataSetIntrospector
allows to interpret DataSet's fields as if they were actual object's fileds
and address them as, for instance, Table1.CustomerName. TCtxInstanceIntrospector
is implemented for Context custom object model, where an object (represented
by TCtxInstance component) considered as collection of Methods (of TCtxScriptCode).
This is very usefull to hold together custom created macros, that can invoke
each other from their scope.
Generally, one never need to inherit from none of the above
introspectors to publish Delphi classes. Context Scripting registers default
introspectors for TComponent, TPersistent and TObject and therefore all the
published properties of any Delphi object will be resolved by them. For
example, such properties as TComponent.Name, TForm.Caption, etc. will be accessible
for any class inherited from those classes.
However, not all the information is available at run-time
(i.e. as RTTI information), so sometimes it is necessary to create TCtxCustomIntrospector(s)
to expose general purpose routines, public methods and properties and even add
more routines and methods to existing classes.
TCtxCustomIntrospector contains an array of method descriptors
associated with a given ClassType. Each method should be declared as TCtxMethod:
TCtxMethod = procedure (Sender: TCtxScript; InvokeType: TCtxInvokeType;
Instance: TObject; ParCount: Integer);
One class type - TCtxSystemScope - is reserved for
system level of static routines. So in order to register a static routine, that
will always be accessible from any script, we need to create and instance of
TCtxCustomIntrospector for TCtxSystemScope and add some methods.
For example, following declaration will register method CreateList
wich creates and returns an instance of TList object and may be considered a
constructor for TList.
procedure _CreateList(Sender: TCtxScript; Instance: TObject;
ParCount: Integer);
begin
// The result of each call, that is supposed to return
result,
// should be assigned to Result register of Sender TCtxScript.
Sender.Result := VarFromObject(TList.Create);
end;
Now we need to register this method, so the script can find
it. This should be done in initialization section of a unit:
initialization
with TCtxCustomIntrospector.Create(TCtxSystemScope)
do
begin
AddMethod('CreateList', @_CreateList, 0 { number of
parameters });
end;
// There's no need to finalize it. The engine will
// dispose all registered introspectors automatically.
end.
Example 1. Now, as we're able to create a TList object,
we need to provide access to its members: Add, Delete, Insert and Count. Here's
the code that does that:
procedure _TListAdd(Sender: TCtxScript; Instance: TObject;
ParCount: Integer);
begin
// Instance contains TList object. First and only parameter contains
// objects to be added to list.
Sender.Result := TList(Instance).Add(VarToObject(Sender.GetParam(1)));
end;
procedure _TListInsert(Sender: TCtxScript; Instance: TObject;
ParCount: Integer);
begin
// Parameters are reversed on stack, so we should get them in
// reverse order.
with Sender do
TList(Instance).Insert(GetParam(2), VarToObject(GetParam(1)));
end;
procedure _TListDelete(Sender: TCtxScript; Instance: TObject;
ParCount: Integer);
begin
TList(Instance).Delete(Sender.GetParam(1));
end;
procedure _TListCount(Sender: TCtxScript; Instance: TObject;
ParCount: Integer);
begin
Sender.Result := TList(Instance).Count;
end;
Now, we need to modify the declarations to add these methods:
initialization
with TCtxCustomIntrospector.Create(TCtxSystemScope)
do
begin
AddMethod('CreateList', @_CreateList, 0 { number of
parameters });
end;
// Note, that methods of TList we register with introspector
for TList
with TCtxCustomIntrospector.Create(TList) do
begin
AddMethod('Add', @_TListAdd, 1);
AddMethod('Delete', @_TListDelete, 1);
AddMethod('Insert', @_TListInsert, 2);
AddProp('Count', @_TListCount);
end;
end.
The above declaration will allow us to execute a Context
Script, instantiating and using TList. For example:
function AddObjectToSomeList(Obj);
var
List;
begin
List := CreateList;
try
List.Add(Obj);
Result := List.Count;
List.Delete(0);
finally
List.Free; // Free method is registered for all objects
by default.
end;
end;
We have published generic TList class. See CtxUnitClasses.pas
unit for examples on how to publish array properties and default array properties.
Source code of all CtxUnitXXX modules is included in trial version.
This can be done, so the user can invoke it from a custom
script/macro. As one can understand from above, it is not necessary for a class
to have certain method, for us to register and publish that method for that
class.
Example 2. Adding a form's method, that performs some
action with form's objects
procedure _AddMemoLine(Sender: TCtxScript; Instance: TObject;
ParCount: Integer);
begin
// This method will add a string passed as only parameter
// to Memo1 contol.
// This simple method will be registered for form class TForm1,
so
// the Instance will conatain an instance of Form1.
TForm1(Instance).Memo1.Lines.Add(Pop);
end;
initialization
with TCtxCustomIntrospector.Create(TForm1) do
begin
AddMethod('AddMemoLine', @_AddMemoLine);
end;
end.
The script to use it may look like this:
procedure Test;
begin
AddMemoLine('Hello World!');
end;
In order to invoke this script in a context of a form, we should either assign
Form1 to CtxScript1.GlobalScope:
CtxScript1.GlobalScope := Form1;
or create script code instance (TCtxScriptCode), that is a method of Form1:
Macro := TCtxScriptCode.Create;
Macro.Code := .... whenever we store our macors
CtxScript1.Execute(Macro, Form1);
The above described introspectors will do most of the job
in standard 'static' Delphi object model. However, sometimes it is beneficial
to extend the object model. For instance, we'd like a user to be able to write
scripts, that directly access TDataSet fields by name without calling FieldByName
function. A script then could look like this:
Customers := OpenTable('Customers');
while not Customers.EOF do
begin
TotalBalance := TotalBalance + Customers.Balance;
Customers.Next;
end;
Example 3. Implementing TDataSet introspector (see
unit CtxUnitDB.pas)
Methods OpenTable, EOF and Next could be done using TCtxCustomIntrospector
in the same manner as described above for TList (we will not repeat this code
here, it is available in CtxUnitDB unit). However, the field Balance
cannot be declared the same way, because it is impossible to declare methods
for every field name imaginable in a database. Therefore we will need to create
a new custom object model, that will allow us to resolve field names within
the scope of TDataSet. This requires inheriting from TCtxIntrospector and implementing
its three methods.
TCtxDataSetIntrospector = class (TCtxIntrospector)
public
// Returns True if this introspector is able to resolve the name
// within the given scope.
function ResolveName(CtxScript: TCtxScript; Scope: TObject;
const Name: String; var Index: Integer): Boolean;
override;
// Invokes set method by index
function Invoke(CtxScript: TCtxScript; InvokeType: TCtxInvokeType;
Scope: TObject; const Name: String; var Index:
Integer;
ParCount: Integer): Boolean; override;
end;
{ TCtxDataSetIntrospector }
function TCtxDataSetIntrospector.ResolveName(CtxScript: TCtxScript;
Scope: TObject; const Name: String; var Index: Integer): Boolean;
var
Fld: TField;
begin
Fld := TDataSet(Scope).FindField(Name);
Result := Fld <> nil; // Return True if field is found
if Result then Index := Fld.Index;
end;
function TCtxDataSetIntrospector.Invoke(CtxScript: TCtxScript;
InvokeType: TCtxInvokeType; Scope: TObject; const Name: String;
var Index: Integer; ParCount: Integer): Boolean;
begin
Result := (Index >= 0) and (Index < TDataSet(Scope).FieldCount);
if not Result then
Result := ResolveName(CtxScript, Scope, Name, Index);
if Result then
case InvokeType of
citGetMethodOrProp:
CtxScript.Result := TDataSet(Scope).Fields[Index].Value;
citSetProp:
TDataSet(Scope).Fields[Index].Value
:= CtxScript.Result;
citGetArrayProp, citSetArrayProp:
InvokeDefaultArrayProp(CtxScript,
InvokeType, Scope, Name,
Index, ParCount);
else CtxScriptErrorFmt(SInvalidInvokeType,
[Name]);
end;
end;
All we have to do now is to register it:
initialization
TCtxDataSetIntrospector.Create(TDataSet);
end.
We're able to work with field names of any component inherited
from TDataSet as if those were its properties. We can even pass that dataset
as a parameter to other macros or methods.
|