| C sharp Eval Examples | 15 May 2010 |
In this article I will give examples of how to use the C# Eval code. For any usage the first thing you will need to do is to choose the configuration. Each of the five configurations is chosen by specifying the appropriate conditional symbols in the project properties. As a refresher, the possible configurations are:
Each configuration has its own program file. For the two Repl configurations this program file will bring up the Repl dialog. For the other four configurations this program file does some tests and then exits. The code in these files gives useful examples of how to use the CsharpEval API.
When incorporating the CsharpEval code into your complete program you have a number of choices:
So let's do some examples. Note that all of these examples appear as a single file in the Download for this article. They have been slightly expanded from the demonstration code that appears in this article; the examples have been placed into methods and arranged as unit tests. The best way to test these is to create a new Visual Studio project containing the CsharpEval code, set the configuration to the full Repl, and also drop into this project the Examples.cs file. Now you can run the Repl, and from the Repl command line explicitly call any of these examples.
All of the CsharpEval code files use the Kamimu namespace. It is assumed in all following examples that either a using statement like
using Kamimu;
is present at the top of the code file, or that your code is inside a
namespace Kamimu
{
...
}
namespace scope.
If you are running any configuration with the Dialog conditional symbol, you will firstly require a statement like
LexToken.ShowError = (msg, theList) =>
{
new LexErrorDialog()
{
Message = msg,
CompilerList = theList,
}.Show();
};
This needs to be executed once at program initialisation. ShowDialog is a static field in the LexToken class and is called to display any errors that CsharpEval may find.
If you are running a configuration without Dialog, you will need to hook up your own error handling. A possible example is
LexToken.ShowError = (msg, theList) =>
{
MessageBox.Show (
msg + "\n" + theList.CodeFormat ,
"Error found" ) ;
};
Naturally you may alter this to your requirements.
You always need at least one TypeParser instance. This specifies to CsharpEval the default namespaces and the referenced assemblies to use. You can have as many as you like, depending upon your requirements. Here is an example:
TypeParser parser = new TypeParser(
Assembly.GetExecutingAssembly(),
new List<string>()
{
"System" ,
"System.Collections.Generic" ,
"System.Linq" ,
"System.Text" ,
"System.Windows" ,
"System.Windows.Shapes" ,
"System.Windows.Controls" ,
"System.Windows.Media" ,
"System.IO" ,
"System.Reflection" ,
"Kamimu"
}
);
The first parameter specifies in which assembly or assemblies the TypeParser will search for types. The TypeParser is set up to reference this assembly and all assemblies directly referred to by this assembly. This corresponds to the 'References' section of a Visual Studio project.
The second parameter specifies the default namespaces to use. This corresponds to the 'using' statements at the start of a C# code file.
CsharpEval can compile and immediately execute the contents of string. For this to work you will need to set the default parser
TypeParser.DefaultParser = parser ;
in some initialisation code. For this example it is assumed that the parser variable is the one created by the call to new TypeParser in the above example.
Now to compile and execute some expression, simply do (for example)
Double d = MakeMethod.DoExpression<double> ( "23.45 / 32" ) ;
This is equivalent to the straight C# code
Double d = 23.45 / 32 ;
Another example:
public class Examples
{
public static string AStr = "A string" ;
private void Test ()
{
string s2=MakeMethod.DoExpression<string>(
"Examples.AStr+Examples.AStr");
}
}
which is equivalent to the straight C# code
public class Examples
{
public static string AStr = "A string" ;
private void Test ()
{
string s2 = AStr + AStr ;
}
}
If there is the possiblity of compiler errors (say if the input being compiled is obtained directly from a user), then use a try block:
double d ;
try {
d = MakeMethod.DoExpression<double> ( "23.45 / 32" ) ;
} catch ( Exception ex ) {
... your error handler code here ...
}
Statements can also be compiled and immediately executed. For example,
public string AStr ;
...
MakeMethod.DoStatement (
"Examples.AStr = \"Some string contents\" ; " ) ;
which will produce the equivalent action as
public string AStr;
...
AStr = "Some string contents" ;
(only slower, of course).
Multiple statements are allowed, as in
public List<string> list = new List<string>() {
"red" , "green" , "red" , "orange" ,
"yellow" , "red" , "green" } ;
public int counter = 0 ;
MakeMethod.DoStatement(@"
Examples.Counter = 0 ;
foreach ( var s in Examples.ColoursList ) {
if (s == ""red"") Examples.Counter = Examples.Counter + 1 ;
}"
);
After execution the counter variable should end up with 3 in it. Note that the two variables are assumed to be declared as instance (or static) fields in the enclosing class. CsharpEval cannot access directly the local variables of a method.
When compiling a method to a delegate, you need to specify the method's signature as a generic type to the CsharpEval MakeMethod class. Here is an example of a function that takes an int parameter and returns a string:
Func<int, string> fn = MakeMethod<Func<int, string>>.
Compile(parser, LexList.Get(@"
public string TheMethod ( int i )
{
return i.ToString() ;
}"
));
This can now be called, as in
string s = fn ( 123 ) ;
or as in
List<int> listOfIntegers = new List<int>() {
99, 120, 4, 134, 18, 19, 200 };
List<string> list =
(from i
in listOfIntegers
where i > 100
select fn(i)
).ToList();
In other words, the delegate can be used like any other delegate in C#.
Another example, this time for a method that takes an int and a double and does not return any value:
public static string StrProperty { get ; set ; }
// In the enclosing class definition
Action<int,double> act = MakeMethod<Action<int,double>>.
Compile ( parser , LexList.Get ( @"
public void TheMethod ( int i , double d )
{
if (Examples.StrProperty != null &&
Examples.StrProperty != """")
{
Examples.StrProperty =
Examples.StrProperty + "","" ;
}
Examples.StrProperty =
Examples.StrProperty + i.ToString() +
"","" + d.ToString() ;
}"
)
) ;
This might be called as in
act ( 1 , 23.4) ;
act ( 2 , 45.23) ;
act ( 99 , 1.23) ;
After these three calls the contents of StrField should be
1,23.4,2,45.23,99,1.23
And, as in one of the examples above, you can surround the MakeMethod.Compile call with try brackets if there is any chance that the compilation may fail.
The MakeMethod.Compile method used above expects a LexList as its parameter.
A LexList can be generated by:
LexList ll = LexList.Get ( @"
public str Convert ( int i )
{
return i.ToString() ;
}"
) ;
string[] lines = new string[] {
"public str Convert ( int i )" ,
"{" ,
" return i.ToString() ; ",
"}" } ;
LexList ll = LexList.Get ( lines ) ;
public LexList MakeTheMethod ( string nameA )
{
LexListBuilder llb = new LexListBuilder () ;
llb.Add ( "public int GetValue()" ) ;
llb.Add ( "{ return Examples." + nameA + "; }" ) ;
return llb.ToLexList() ;
}
So now calling MakeTheMethod ( "A" ) will return a LexList identical to the one returned by
LexList.Get ( @"public int GetValue ()
{ return Examples.A ; }" ) ;
public LexList MakeTheMethod ( string nameA )
{
LexListBuilder llb = new LexListBuilder () ;
llb.Add ( "public int GetValue()" ) ;
llb.Add ( "{ return 'AA ; }" ,
"AA" , nameA ) ;
return llb.ToLexList() ;
}
The identifier called AA in the string has a single quote in front of it. This indicates that it is to be used as a substitution parameter. The last two arguments to the Get call specify this parameter name and the value to substitute for it. In this example the 'AA will be replaced by the contents of nameA. So if the above MakeTheMethod is called by
MakeTheMethod( "Var1" ) ;
the resulting LexList will be the same as the one returned by the following
LexList ll = LexList.Get (
"public int GetValue () { return Examples.Var1 ; }" ) ;
Any number of Parameter name and actual value pairs may be used. If the parameter name is used more than once in the string, all occurences will be replaced. Any parameter name and actual value pair that does not have a corresponding parameter in the string is just ignored. A parameter name must be more than one character long. Another example:
public LexList MakeTheMethod (
string TheName , string TheOtherName )
{
LexListBuilder llb = new LexListBuilder () ;
llb.Add (
@"public int GetValue ()
{
return Alpha.'TheName +
Alpha.'TheOtherName + Alpha.'TheName ;
}" ,
"TheName" , TheName ,
"TheOtherName" , TheOtherName ) ;
return llb.ToLexList() ;
}
Calling this with the call
MakeTheMethod ( "A" , "B" )
will produce a LexList that is identical to the one produced by the following call:
LexList.Get (
@"public int GetValue () {
return Alpha.A + Alpha.B + Alpha.A ; }" ) ;
Note how I have chosen the parameter name, internal to the string argument passed to the LexList.Get method, to be the same as the corresponding name of the MakeTheMethod formal parameter. You might as well do it like this, to avoid having to think up two separate names. As long as you remember that, as far as the C# compiler and the LexList analyser are concerned, they are totally distinct.
Token pasting is supported. Joining a substituted token onto the end of an identifier is done by
llb.Add ( "return First'TheName ;" , "TheName" , "Value" ) ;
This will produce a LexList equivalent to the one produced by
LexList.Get ( "return FirstValue ;" ) ;
To join a token onto the front of an identifier, do the following:
llb.Add ( "return 'TheName''Last ; " , "TheName" , "Value" ) ;
Here the two ' quotes one after the other indicate that the substitution parameter has finished and that the next character is part of a normal identifier. The above example is equivalent to
LexList.Get ( "return ValueLast ; " ) ;
To join two parameters together:
llb.Add ( "return 'NameOne'NameTwo ; " ,
"NameOne" , "One" , "NameTwo" , "Two" ) ;
is equivalent to
LexList.Get ( "return OneTwo ;" ) ;
Here is a final example of token pasting:
llb.Add ( "return First'NameOne'NameTwo'NameThree''Last ; " ,
"NameOne" , "One" ,
"NameTwo" , "Two" ,
"NameThree" , "Three" ) ;
which is equivalent to
LexList.Get ( "return FirstOneTwoThreeLast ; " ) ;
You are not forced to use token substitution, you can just concatenate the strings together using any of the normal C# string handling techniques. However it is useful in making the code easier to understand.
An extension to token substitution is the ability to substitute a type directly into the LexList being constructed. Consider the example
llb.Add ( "return typeof (MyClassType) ;" ) ;
Using this you are relying on the TypeParser instance having access to the correct MyClassType type. Sometimes this may not always be possible or reliable. For example, MyClassType may be in a different assembly or namespace from the ones accessible in the TypeParser instance. Or you may have several MyClassType's in different namespaces. Or the type you want to use may need to be specified by a long sequence of namespace and class type identifiers. To remove all ambiguity you can substitute in the type directly, as in
llb.Add ( "return typeof(`Type) ;",
"Type" , typeof(MyClassType) ) ;
This will expand to the same line as the previous example. The difference is that you are now confident that whatever the type you used as the parameter of this call will be the type that the CsharpEval compiler sees when it evaluates the final LexList.
The LexListBuilder Add method will also take a LexListBuilder argument, or a LexList argument. It also provides something called quote promotion. Consider the following:
LexListBuilder llb = new LexListBuilder () ;
llb.Add (
"string s = \"The string contents\" + 'Variable ; " ,
"Variable" , ActualValue ) ;
llb.Add (
@"string anotherStr = ""Another string contents"" ; ") ;
llb.Add ( "char ch = 'c' ; " ) ;
... and so on ...
Sometimes it is desired to avoid all of that escaping for the double quotes. So the following can be used in its place:
LexListBuilder llb = new LexListBuilder () ;
llb.AddAndPromoteQuotes (
"string s = 'The string contents' + `Variable ; " ,
"Variable" , ActualValue ) ;
llb.AddAndPromoteQuotes (
"string anotherStr = 'Another string contents' ;" ) ;
llb.AddAndPromoteQuotes ( "char ch = `c` ; " ) ;
... and so on ...
The AddAndPromoteQuotes method will promote the ' quote to a " quote, and the ` quote to a ' quote. This quote promotion is purely local to this method call, so you are free to mix calls to Add and to AddAndPromoteQuotes. The final LexList will have the normal ' and " quotes.
To attach new methods and fields to an existing class requires the Class compiler symbol to be used.
For these examples we shall be using the following example class:
public class TestClass
{
public int TheInt;
public string TheString;
public int GetTheInt()
{
return TheInt;
}
public List<object> Fields;
}
The following example adds two new methods to this:
MakeClass mc = new MakeClass(parser, LexList.Get(@"
partial class Examples.TestClass
{
public int GetTwoTimesTheInt ()
{
return TheInt * 2 ;
}
public void SetTheString ( string s )
{
TheString = s ;
}
}"));
This will create the two new methods. We now need to obtain the delegates to these methods. To do this we first declare the delegates (either as local variables or as fields):
Func<TestClass,int> GetTwoTimesTheInt ;
Action<TestClass,string> SetTheString ;
Notice how the TestClass type is mentioned explicitly in the signatures.
Then we obtain the delegate values from the mc variable:
mc.GetFunc<TestClass,int>(
"GetTwoTimesTheInt", out GetTwoTimesTheInt ) ;
mc.GetAction<TestClass,string>(
"SetTheString",out SetTheString ) ;
At this point the delegate variables GetTwoTimesTheInt and SetTheString have been assigned valid delegates and can be used. For example,
TestClass tc = new TestClass () ;
tc.TheInt = 34 ;
int i = GetTwoTimesTheInt ( tc ) ;
// i is now 68 ;
SetTheString ( tc, "New string value" ) ;
// tc.TheString now has "New string value"
The GetFunc and the GetAction methods return the MakeClass instance. So it is possible to run together the calls:
mc.
GetFunc<TestClass,int>(
"GetTwoTimesTheInt", out GetTwoTimesTheInt ).
GetAction<TestClass,string>(
"SetTheString",out SetTheString ) ;
It is therefore also possible to attach these calls to the instantiation of the MakeClass instance, as in
MakeClass mc = new MakeClass(parser, LexList.Get(@"
partial class Examples.TestClass
{
public int GetTwoTimesTheInt ()
{
return TheInt * 2 ;
}
public void SetTheString ( string s )
{
TheString = s ;
}
}")).
GetFunc<TestClass, int>(
"GetTwoTimesTheInt", out GetTwoTimesTheInt).
GetAction<TestClass, string>(
"SetTheString", out SetTheString);
The methods you add can access any public methods and fields and properties of the real TestClass instance. They can also access any of the attached methods (and fields). You can also attach some methods and then at a later point attach some more. Thus
Action<TestClass, string, int> SetStringAndInt;
mc.AddMethodsAndFields(LexList.Get(@"
partial class Examples.TestClass
{
public void SetStringAndInt ( string s , int i )
{
TheInt = i ;
SetTheString ( s ) ;
}
}"), true).
GetAction<TestClass, string, int>(
"SetStringAndInt", out SetStringAndInt);
Calling the SetStringAndInt delegate will in turn call the SetTheString delegate as part of its actions.
You can provide a new implementation for an already existing method, as long as the method signature is the same. It needs to be the same since I have not implemented overloading of methods. So taking the above example, you can redefine the SetStringInt method with, for example,
mc.AddMethodsAndFields(LexList.Get(@"
partial class Examples.TestClass
{
public void SetStringAndInt ( string s , int i )
{
TheInt = i * 100 ;
SetTheString ( s ) ;
}
}"), true);
Since the delegate already exists, there is no need to fetch it again. The true parameter at the end of each call to AddMethodsAndFields allows methods to be redefined. If this is false, then an attempt to redefine an existing method will generate an exception.
To attach new fields to the TestClass, you need to first add a List<object> Fields declaration to the real TestClass code. So the new TestClass we will be using is
public class TestClass
{
public int TheInt ;
public string TheString ;
public int GetTheInt ()
{
return TheInt ;
}
public List<object> Fields ;
}
And now adding a new field is easily done:
Func<TestClass, int> GetIntValue;
Action<TestClass> Init;
MakeClass mc = new MakeClass(parser, LexList.Get(@"
partial class Examples.TestClass
{
public int LastIntValue ;
public int GetIntValue ()
{
LastIntValue = TheInt ;
return TheInt ;
}
}")).
GetFunc<TestClass, int>("GetIntValue", out GetIntValue).
GetAction<TestClass>("FieldsInitialiser", out Init);
Notice the appearance of the FieldsInitialiser action. This is automatically supplied to the MakeClass code and must be called at least once for every instance of TestClass we are using the newly added methods on. It will initialise the contents of the Fields list in the real TestClass.
So to use this GetIntValue delegate:
TestClass tc1 = new TestClass () ;
tc1.TheInt = 22 ;
Init(tc1) ;
int i = GetIntValue(tc1) ;
// i is 22 and so is LastIntValue
tc1.TheInt = 33 ;
int j = GetIntValue(tc1) ;
// j is 33 and so is LastIntValue
TestClass tc2 = new TestClass () ;
Init(tc2) ;
tc2.TheInt = 100 ;
int k = GetIntValue ( tc2) ;
// k is 100 and so is LastIntValue
Note that the Init delegate was called for each instance of TestClass. Each instance has its own Fields list, and each must be initialised.
Having defined one method and field for this MakeClass instance, you can add more:
Action<TestClass, string> AddString;
mc.AddMethodsAndFields(LexList.Get(@"
partial class Examples.TestClass
{
public List<string> ListOfStrings ;
public void AddString ( string s )
{
if (ListOfStrings == null)
ListOfStrings = new List<string> () ;
ListOfStrings.Add ( s ) ;
}
}"), true).
GetAction<TestClass, string>("AddString", out AddString);
Remember, you must call the FieldsInitialiser method after every time a new field is added to the class, and this must be called for every instance of the class you are using.
Article text is under the Creative Commons Zero license. Source codes are under the MIT license.
Comments