|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionWhile converting some software to .NET, I came across the need for a user defined calculation or scripting class. In the software I was working on, in places where there are formulas for performing engineering calculations, the user can add his/her own formula rather than just select from built-in methods. In the past, I accomplished this task using a system originally written in assembly and modeled on the Forth language[^]. Over the years, the system was converted to use algebraic notation and ported to Pascal, C, C++, VB and a few other languages. The threaded interpreted language[^] model provided for fast interpretation and execution speed rivaling compiled code, which is important when allowing user defined functions to process millions of data points. In converting to .NET, however, it seems there is a better way to do things using reflection. Essentially it allows one to embed the standard .NET compilers directly in the program and compile user input "on-the-fly." As a result, rather than once again converting my calculation engine to a new language, I implemented a calculation engine that allows user functions to be written in any available .NET language. Using the ScriptEngine ClassThe project download contains the Since the purpose of this engine is to perform engineering calculations, all variables are defined as
Note that the user code should set a value for the Result = Double.NaN
The general procedure for using In my applications, the usual use of ScriptEngine Engine = new ScriptEngine(ScriptEngine.Languages.VBasic);
Engine.Code = code;
Engine.AddVariable("X");
Engine.AddVariable("Y");
if (Engine.Compile())
{
foreach (ValueType v in Values)
{
// Set the variable values for the script
Engine.SetVariable("X", v.X);
Engine.SetVariable("Y", v.Y);
// Evaluate the script and return the Result
double result = Engine.Evaluate();
// Retrieve any variables that might have changed
double x = Engine.GetVariable("X");
// Do something with the result }
}
}
else
{
MessageBox.Show("Compiler message: " + Engine.Messages[0]);
}
Inside ScriptEngineTo do its job, the The rest of the generated code defines a The C# code that actually compiles and evaluates the generated code is as follows: public bool Compile()
{
switch (Language)
{
case Languages.CSharp:
source = "namespace UserScript\r\n{\r\nusing System;\r\n" +
"public class RunScript\r\n{\r\n" +
variables + "\r\npublic double Eval()\r\n{\r\ndouble Result =
Double.NaN;\r\n" +
code + "\r\nreturn Result;\r\n}\r\n}\r\n}";
compiler = new CSharpCodeProvider();
break;
case Languages.JScript:
source = "package UserScript\r\n{\r\n" +
"class RunScript\r\n{\r\n" +
variables + "\r\npublic function Eval() :
String\r\n{\r\nvar Result;\r\n" +
code + "\r\nreturn Result; \r\n}\r\n}\r\n}\r\n";
compiler = new JScriptCodeProvider();
break;
case Languages.FSharp:
source = "#light\r\nmodule UserScript\r\nopen System\r\n" +
"type RunScript() =\r\n" +
" let mutable Result = Double.NaN\r\n" +
variables + "\r\n" + variables1 +
" member this.Eval() =\r\n" +
code + "\r\n Result\r\n";
compiler = new FSharpCodeProvider();
break;
default: // VBasic
source = "Imports System\r\nNamespace
UserScript\r\nPublic Class RunScript\r\n" +
variables + "Public Function Eval()
As Double\r\nDim Result As Double\r\n" +
code + "\r\nReturn Result\r\nEnd Function\r\nEnd Class\
r\nEnd Namespace\r\n";
compiler = new VBCodeProvider();
break;
}
parameters = new CompilerParameters();
parameters.GenerateInMemory = true;
results = compiler.CompileAssemblyFromSource(parameters, source);
// Check for compile errors / warnings
if (results.Errors.HasErrors || results.Errors.HasWarnings)
{
Messages = new string[results.Errors.Count];
for (int i = 0; i < results.Errors.Count; i++)
Messages[i] = results.Errors[i].ToString();
return false;
}
else
{
Messages = null;
assembly = results.CompiledAssembly;
if (Language == Languages.FSharp)
evaluatorType = assembly.GetType("UserScript+RunScript");
else
evaluatorType = assembly.GetType("UserScript.RunScript");
evaluator = Activator.CreateInstance(evaluatorType);
return true;
}
}
public double Evaluate()
{
object o = evaluatorType.InvokeMember(
"Eval",
BindingFlags.InvokeMethod,
null,
evaluator,
new object[] { }
);
string s = o.ToString();
return double.Parse(s.ToString());
}
As can be seen, a Specifically, in F# the indentation is important when using the For illustration, the generated code to return a vector magnitude given In C#: namespace UserScript
{
using System;
public class RunScript
{
double X = 0;
public void SetX(double x) { X = x; }
public double GetX() { return X; }
double Y = 0;
public void SetY(double x) { Y = x; }
public double GetY() { return Y; }
public double Eval()
{
double Result = Double.NaN;
Result = Math.Sqrt(X*X + Y*Y); // This is the user code
return Result;
}
}
}
In JScript: package UserScript
{
class RunScript
{
var X : double;
public function SetX(x) { X = x; }
public function GetX() : String { return X; }
var Y : double;
public function SetY(x) { Y = x; }
public function GetY() : String { return Y; }
public function Eval() : String
{
var Result;
Result = Math.sqrt(X*X + Y*Y); // This is the user code
return Result;
}
}
}
In Visual Basic: Imports System
Namespace UserScript
Public Class RunScript
Dim X As Double
Public Sub SetX(AVal As Double)
X = AVal
End Sub
Public Function GetX As Double
Return X
End Function
Dim Y As Double
Public Sub SetY(AVal As Double)
Y = AVal
End Sub
Public Function GetY As Double
Return Y
End Function
Public Function Eval() As Double
Dim Result As Double
Result = (X*X + Y*Y)^0.5 ' This is the user code
Return Result
End Function
End Class
End Namespace
In F#: #light
module UserScript
open System
type RunScript() =
let mutable Result = Double.NaN
let mutable X = 0.0
let mutable Y = 0.0
member x.GetX = X
member x.SetX v = X <- v
member x.GetY = Y
member x.SetY v = Y <- v
member this.Eval() =
Result <- Math.Sqrt(X*X + Y*Y) // This is the user code
Result
Purging the AppDomainPer an excellent observation by Uwe Keim, I added a static In most of my applications this does not seem to be a problem, but Uwe is correct in pointing out that using In addition, to avoid conflict with multiple instances of ConclusionsFor the purpose of implementing user defined engineering calculations, As far as performance, it's hard to tell the difference in execution between the user defined code and regular compiled code, other than a slight lag while compiling the code, since the user defined code is actually compiled. I didn't make a concerted effort to find out why, but it does seem that the F# code executes somewhat slower than the other languages. That may be due to the preliminary nature of the current F# compiler or due to the extra overhead involved in mapping F# syntax to .NET. Time will tell if future releases of F# perform better. In addition, using the methods illustrated here, it is fairly easy to produce other special purpose code generated at run time. Although I'm not generally a fan of dynamic code due to the probability of introducing near impossible debugging problems, there certainly are cases where run time code generation is needed. I hope that by using And for me, a major advantage is that my Users Manuals can be simplified significantly, since I don't have to document all of the scripting language. Instead I can simply explain the use of the variables, that the result is returned in the And as always, I can't claim the code presented here is optimal. If anyone has any suggestions or observations, I'd be pleased to hear about them. History
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||