In this article, I'm going to talk about using a custom object-oriented scripting in C#. By “custom,” I mean that all you're going to see here is available to use and modify from GitHub. By “C#,” I mean that the scripting language is implemented in C# and you can just include it in your project in order to adjust it as you wish. As a scripting language, I'm going to use CSCS (customized scripting in C#).
I've talked about this language in a few previous CODE Magazine articles (see links in the sidebar). CSCS is an open-source scripting language that's very easy to integrate into your C# project.
You're going to see how to use classes and objects in scripting, and also how they're implemented in C#. It's important that you have a full control of how the object-oriented functionality is implemented. For instance, you can have multiple inheritance in scripting, which is forbidden in C# or in JavaScript. But you could also disable it if you think that it's against your beliefs. It's important that you, and not another architect, decide what features you want to have to solve a particular problem.
The great thing about object-oriented code is that it can make small, simple problems look like large, complex ones. – Anonymous
As an example of using object-oriented scripting, I'm going to take a look at a client-server application, where I'll show how you can send and receive objects. I'll also show a simple marshalling-unmarshalling mechanism (converting objects to a string and back) to pass data across the wire. You can use a similar approach for any custom client-server communication, just using a couple of lines of a scripting code.
To distinguish between the C# code and CSCS scripting code, all C# code is provided below with the syntax highlighting, whereas all scripting code doesn't use it.
Let's start by looking at how you can set up scripting in your .NET Visual Studio project.
Setting Up CSCS Scripting
One of the simplest ways to start using CSCS scripting is to download the source code from GitHub (see https://github.com/vassilych/cscs) and add the source code directly to your C# .NET project. The license lets you modify and use the code without any restrictions.
An example of including the CSCS Scripting Engine in a Windows GUI project is a WPF project, available here: https://github.com/vassilych/cscs_wpf
Another example is a Xamarin iOS - Android mobile project that can be downloaded from here: https://github.com/vassilych/mobile.
CSCS is a functional language, syntactically very similar to JavaScript. To add a new functionality to CSCS, you'll need to perform just these three steps:
- Define a CSCS function name as a constant. When parsing this constant, the CSCS parser triggers the appropriate implementation code.
- Implement a new class, deriving from the
ParserFunction
class. The most important method isEvaluate()
. It will be triggered when the constant defined in the previous step is parsed. - Register the newly created class with the parser.
Let's see how this is done using the implementation of the power function xy as an example.
First, you define an appropriate constant in the Constants.cs
file:
public const string MATH_POW = "Math.Pow";
Next, you define the implementation:
class PowFunction : ParserFunction {
protected override Variable Evaluate(ParsingScript script) {
List<Variable> args = script.GetFunctionArgs();
Utils.CheckArgs(args.Count, 2, m_name, true);
Variable arg1 = args[0];
Variable arg2 = args[1];
arg1.Value = Math.Pow(arg1.Value,arg2.Value);
return arg1;
}
public override string Description() {
return "Returns a specified number \ raised to the specified power.";
}
}
Finally, the last step is registering this new functionality with the parser at the program initialization stage:
ParserFunction.RegisterFunction(Constants.MATH_POW, new PowFunction());
You're done now. As soon as the parser sees something like Math.Pow(2, 5)
, the Evaluate()
method above is triggered and the correct value of 32 calculated.
The Description
method is triggered when the user calls a Help
scripting method.
Object-oriented programming had boldly promised “to model the world.” Well, the world is a scary place where bad things happen for no apparent reason, and in this narrow sense I concede that OO does model the world. – Dave Fancher
Note the convenient method script.GetFunctionArgs()
. It returns all comma-separated arguments between the parentheses (e.g., it returns 2
and 5
for Math.Pow(2, 5)
). You can also put some variables and arrays as function arguments - their value will be recursively extracted during the GetFunctionArgs()
call.
In the next sections, I'm going to show how you can use the new function definition shown in this section to define classes and objects.
“Hello, World!” in Object-Oriented Scripting
Let's first see how classes and objects are defined and used in scripting and then how they are implemented in C#.
I hope you'll find this very intuitive and similar to other languages, with some few exceptions (like multiple inheritance).
With enough practice, any interface is intuitive. – Anonymous
Let's see two simple examples of a class definition in CSCS:
class Stuff1 {
x = 2;
Stuff1(a) {
x = a;
}
function helloWorld() {
print("Hello, World!");
}
}
class Stuff2 {
y = 3;
Stuff2(b) {
y = b;
}
function addStuff2(n) {
return n + y;
}
}
You can now create new objects and use these classes as usual:
obj1 = new Stuff1(10);
obj2 = new Stuff2(5);
print(obj1.X + obj2.Y); // prints 15.
print(obj1); // prints stuff1.obj1[x=10]
Now let's use multiple inheritance, something you can't do in many modern languages. Let's define a class that inherits both the method implementations and variables from the base classes:
class CoolStuff : Stuff1, Stuff2 {
z = 3;
CoolStuff(a=1, b=2, c=3) {
x = a;
y = b;
z = c;
}
function addCoolStuff() {
return x + addStuff2(z);
}
function ToString() {
return "{" + x + "," + y + "," + z + "}";
}
}
Here's how you can use this newly defined class:
obj3 = new CoolStuff(11, 22, 33);
obj3.HelloWorld(); // prints "Hello, World!"
print(obj3.AddStuff2(20)); // prints 42
print(obj3); // prints {11,22,33}
As you can see, both variables and methods can be used from the base classes. A special method is ToString()
. When defined, it overrides the string representation of the object (e.g., what's printed in print(object)
statement). The default ToString()
implementation is the following: ClassName.InstanceName[variable,variable2,...]
.
You probably noted that some of the class methods start with a lowercase letter, others with an uppercase: it doesn't matter, CSCS scripting language is case insensitive.
CSCS scripting language is case-insensitive.
You can also debug a CSCS script. The easiest method is to install the CSCS Debugger and REPL Extension for Visual Studio Code (https://marketplace.visualstudio.com/items?itemName=vassilik.cscs-debugger). This CODE Magazine article explains how to use Visual Studio Code Extensions for debugging: Writing Your Own Debugger and Language Extensions with Visual Studio Code.
Figure 1 shows a debugging session with some CSCS scripting statements.
Implementing Scripting Classes and Objects in C#
Let's see briefly how the classes and objects scripting functionality from the previous section is implemented in C#. As you previously saw with the Math.Pow()
example, all of the CSCS functionality is implemented as functions. Yes, even classes are implemented this way, no matter how strange it sounds.
When the CSCS parser reads a class definition, that starts with Class ClassName ..., the C# implementation is triggered (see Listing 1).
Listing 1: C# Code to create a scripting class
public class ClassCreator : ParserFunction
{
protected override Variable Evaluate(ParsingScript script)
{
string className = Utils.GetToken(script);
string[] baseClasses = Utils.GetBaseClasses(script);
var newClass = new CSCSClass(className, baseClasses);
newClass.ParentOffset = script.Pointer;
newClass.ParentScript = script;
string scriptExpr = Utils.GetBodyBetween(script,
Constants.START_GROUP, Constants.END_GROUP);
string body = Utils.ConvertToScript(Utils.GetBodyBetween(
script, Constants.START_GROUP, Constants.END_GROUP, out _);
ParsingScript tempScript = script.GetTempScript(body);
tempScript.CurrentClass = newClass;
tempScript.DisableBreakpoints = true;
var result = tempScript.ExecuteScript();
return result;
}
}
The code in Listing 1 defines a new class, which can now be instantiated in CSCS. This also needs to be registered with the parser before being used:
ParserFunction.RegisterFunction(Constants.CLASS, new ClassCreator());
// CLASS is defined as "class"
As soon as the CSCS parser sees this statement, obj1 = new Stuff1…, another C# implementation is triggered, namely the Evaluate()
method of the NewOjectFunction
class (see Listing 2).
Listing 2: C# code for the new object implementation
public class NewObjectFunction : ParsingFunction
{
protected override Variable Evaluate(ParsingScript script)
{
string className = Utils.GetToken(script);
className = Constants.ConvertName(className);
List<Variable> args = script.GetFunctionArgs();
var csClass = CSCSClass.GetClass(className) as CompiledClass;
if (csClass != null) {
ScriptObject obj = csClass.GetImplementation(args);
return new Variable(obj);
}
var instance = new CSCSClass.ClassInstance(
script.CurrentAssign, className, args, script);
var newObject = new Variable(instance);
newObject.ParamName = instance.InstanceName;
return newObject;
}
}
The NewObjectFunction
must also be registered with the CSCS parser as follows:
ParserFunction.RegisterFunction(Constants.NEW, new NewObjectFunction());
// NEW is defined as "new"
I encourage you to take a look at the CSCS GitHub page (https://github.com/vassilych/cscs) for more implementation details.
Next, let's see an example of using scripting to access Web Services.
Accessing Web Services from Scripting
As an example of accessing a Web Service, you're going to use Alpha Vantage Web Service (https://www.alphavantage.co). Alpha Vantage provides a financial market data API.
The main advantages of using Alpha Vantage are that it's pretty straightforward to create a request and that it's also free to use (well, up to five requests per minute or 500 requests per day, as of this writing). To replicate what you're doing here, you need to request a free API key here: https://www.alphavantage.co/support/#api-key.
Here is how you create a URL to access their Web Service in CSCS:
baseURL = "https://www.alphavantage.co/" +
"query?function=TIME_SERIES_DAILY&symbol=";
apikey = "Y12T0TY5EUS6BXXX";
symbol = "MSFT";
stockUrl = baseURL + symbol + "&apikey=" + apikey;
As a result, you'll get a JSON file (see an example in Listing 3).
Listing 3: JSON string returned from the Alpha Vantage Web Service
{
"Meta Data": {
"1. Information": "Daily Prices (open, high, low, close) and Volumes",
"2. Symbol": "MSFT",
"3. Last Refreshed": "2022-05-26 16:00:01",
"4. Output Size": "Compact",
"5. Time Zone": "US/Eastern"
},
"Time Series (Daily)": {
"2022-05-26": {
"1. open": "262.2700",
"2. high": "267.0000",
"3. low": "261.4300",
"4. close": "265.9000",
"5. volume": "24960766"
},
"2022-05-25": {
"1. open": "258.1400",
"2. high": "264.5800",
"3. low": "257.1250",
"4. close": "262.5200",
"5. volume": "28547947"
},
...
}
};
This is the function to create a Web Request using CSCS scripting:
WebRequest(Request, Url, Load, Tracking,
OnSuccess, OnFailure, ContentType, Headers);
Here is what these parameters mean:
Request
is one of the standards GET, POST, PUT, etc. For Alpha Vantage, you need GET.- The Web service
URL
, as defined above. - The
Load
is some additional data to send. - The
Tracking
variable is needed for multiple requests. When you get a response back, theTracking
variable associates it with the right request. OnSuccess
andOnFailure
are CSCS callback methods triggered when the response is received.- The content type by default is application/x-www-form-urlencoded.
- You can also send some headers with the request. This is useful for the REST API requests.
All parameters, except Request
and URL
, are optional. If the OnSuccess
and OnFailure
callback methods aren't supplied, the request is executed synchronously and the result of the request is returned from the WebRequest
method.
An example of accessing the Alpha Vantage Web Service is the following:
result = WebRequest("GET", stockUrl, "", symbol);
The result of this call is shown in Listing 3 for the Microsoft stock. To be able to use the returned JSON string, there's an auxiliary CSCS GetVariableFromJSON()
function. After applying this function, the main parts of the JSON string are split into a list and their subparts can be accessed by a key. Here is how you can access the resulting string (see Listing 3 for details):
function processResponse(text)
{
if (text.contains("Error")) {return text;}
jsonFromText = GetVariableFromJSON(text);
metaData = jsonFromText[0];
result = jsonFromText[1];
symbol = metaData["2. Symbol"];
last = metaData["3. Last Refreshed"];
allDates = result.keys;
dateData = result[allDates[0]];
myStock = new Stock(symbol, last, dateData);
return myStock;
}
The processResponse()
function returns a Stock
object. Its class definition is shown below. The main work of processing the results is in the Stock
class constructor. Here is the Stock
class definition:
class Stock {
symbol = "";
date = "";
open = 0;
low = 0;
high = 0;
close = 0;
volume = 0;
Stock(symb, dt, data) {
symbol = symb;
date = dt;
open = Math.Round(data["1. open"], 2);
high = Math.Round(data["2. high"], 2);
low = Math.Round(data["3. low"], 2);
close = Math.Round(data["4. close"],2);
volume = data["5. volume"];
}
}
Additionally, you can define a custom function for converting the Stock
object into a string. An example of such a function is the following (this method should be added inside of the Stock
class definition):
function ToString()
{
return symbol +" "+ date + ". Open: " + open +
", Close: " + close + ": Low: " + low +
", High: " + high + ", Volume: " + Volume;
}
Now add the following CSCS code:
result = WebRequest("GET", stockUrl, "", symbol);
stock = processResponse(result);
print(stock);
This prints the returned Stock
object according to the ToString()
method defined:
MSFT 2022-05-26 16:00:01. Open: 262.27, Close: 265.9: Low: 261.43, High: 267,
Volume: 24960766
Before getting into the main example of this article, the Client-Server communication, let's take a look at marshalling and unmarshalling objects in CSCS scripting.
Marshalling and Unmarshalling Objects
Using CSCS scripting, you can convert any object or variable to a string and back using these methods: Marshal(object)
and Unmarshal(string)
.
The converted string looks like a simplified XML, but it's not XML. You can tweak the C# implementation code a bit if you want it to be a legal XML string.
The only place where you should really use XML is your resume. – Anonymous
Here's an example of marshalling a Stock
object from the previous section:
mystock = processResponse(r);
ms = Marshal(mystock);
// Returns:
// <mystock:class:stock><symbol:STR:"MSFT">
// <date:STR:"2022-05-27"><open:NUM:268.48>
// <low:NUM:267.56><high:NUM:273.34>
// <close:NUM:273.24><volume:STR:"26910806">
ms.type; // Returns STRING
Here's how you construct an object back from a string:
ums = Unmarshal(ms);
ums.type; // Returns
// SplitAndMerge.CSCSClass+ClassInstance: Stock
You can also marshal and unmarshal any other data structures:
str = "a string";
mstr = Marshal(str); // Returns: <str:STR:"a string">
umstr = Unmarshal(mstr);
int = 13;
mint = Marshal(int); // Returns: <int:NUM:13>
umint = Unmarshal(mint);
The marshalling and unmarshalling is done recursively. Here's an example of an array (which is also a map for some elements) where one of the elements of the original array is an array itself (note that in general, that the data in an array doesn't have to be of the same type):
a[0]=10;
a[1]="blah";
a[2]=[9, 8, 7];
a["x:lol"]=12;
a["y"]=13;
ma = marshal(a);
maa = unmarshal(ma);
// Returns:
// ["x:lol":10, "y":11, 10, "blah", [9, 8, 7]]
maa.type; // Returns ARRAY
Now you're ready for the main example of this article: sending and receiving objects between a server and a client, all implemented in scripting.
A Client-Server Example
The client server example encompasses what you've seen before: a Web Server client, marshalling and unmarshalling objects, and processing JSON strings.
Sample server code does the following for each connected client: in case the request is equal to stock
, the server interprets the load parameter as the stock name (e.g., MSFT) and then sends a stock request to the Alpha Vantage Web Service that I discussed earlier. After receiving the data, the server sends back the Stock
object containing all the stock data fields.
To start a server, you just need to call a startsrv()
scripting function, supplying as arguments a function to be triggered on each client connection and a port where the server is going to listen for the incoming requests. With each request, the server expects the request name and a load object (which can be an array of arguments).
Here's the scripting server-side code:
counter = 0;
function serverFunc(request, obj) {
counter++;
if (request == "stock") {
stockUrl = baseURL + obj + "&apikey=" + apikey;
print(counter + ", request: " + stockUrl);
data = WebRequest("GET", stockUrl, "", symbol);
result = processResponse(data);
return result;
}
}
startsrv("serverFunc", 12345);
Note that you can change the server scripting method to be executed on each client connection on the fly without restarting the server. You can just update and redefine the serverFunc
method (e.g., by using the VS Code CSCS REPL extension, mentioned earlier).
You can update the server scripting code on the fly without restarting the server.
On the scripting client-side, the connecting code looks like this:
response = connectsrv(request, load, port, host = "localhost");
(If the server host isn't supplied, the local host is used for connections). Let's see an example of accessing the server defined above:
response = connectsrv("stock", "MSFT", 12345);
print(response.Symbol + ": Close: " +
response.Close + ", Volume: " + response.Volume);
// MSFT: Close: 273.24, Volume: 26910806
As you can see, the resulting object is returned directly from the connectsrv()
call because all of the marshalling and unmarshalling is done by the scripting framework.
Wrapping Up
The main advantages of using a scripting module inside of your projects are:
- You'll save time when writing code because most of the code is usually much shorter than it would've been for making the same functionality in C#. This is what you saw with the Client-Server example.
- You can use any features not available directly in C# (e.g., multiple inheritance).
- You can modify scripting code on the fly without the necessity of recompiling and restarting the service.
I'm looking forward to your feedback, especially how you use CSCS scripting in your projects, what Web Services you access, and any performance tricks you're using.