Click here to Skip to main content
Click here to Skip to main content

Making .NET Applications Scriptable with Aphid, an Embeddable Scripting Language

, 29 Nov 2013
Rate this:
Please Sign up or sign in to vote.
This article details how to make .NET applications scriptable with Aphid, an embeddable scripting language.

Introduction

Aphid is an embeddable, cross-platform, multi-paradigm, and highly interoperable .NET scripting language. The Aphid interpreter is implemented entirely in C#. This article is intended to be an introduction to Aphid, and as such, only covers some of the features available. Further, Aphid is currently in an alpha state, so as it evolves, expect this article to change and grow with it. For the most recent version of Aphid, visit the CodePlex page.

What Languages is Aphid Inspired By?

Aphid is C-style language heavily inspired by JavaScript. However, it does draw from C#, and to a lesser extent, F#.

Why Another Scripting Language?

Currently, few easily embeddable scripting languages exist for the .NET platform. Among those available, many have several dependencies, necessitating the inclusion of various assemblies. Still others are lacking in interoperability, requiring inordinate amounts of wire-up code. Aphid seeks to solve these problems by providing an easily embeddable, highly interoperable scripting language contained within a single DLL.

Aphid Types

Aphid has seven types: string, number, boolean, list, object, function, and null. The table below shows how Aphid's types are mapped to .NET's types.

Aphid Type .NET Type
string System.String
number System.Decimal
boolean System.Boolean
list System.Collections.Generic.List<T>
object Components.Aphid.Interpreter.AphidObject
function Components.Aphid.Interpreter.AphidFunction
null null

Strings

String literals can be written using either single quotes or double quotes. The backslash character (\) is used in escape sequences.
x = 'foobar\r\n';

Numbers

Number literals can be written in decimal or hexadecimal form.
x = 1;

x = 3.14159265359;

x = 0xDEADBEEF;

Booleans

Boolean values can be specified using the true or false keywords.
x = true;

x = false;

Lists

Lists are comprised of comma (,) delimited elements enclosed in square brackets ([]).
x = [ 5, 2, 3, 0 ];

Objects

Objects are comprised of comma (,) delimited name-value pairs enclosed in braces ({}). Names and values are separated by a colon (:).
widget = {
    name: 'My Widget',
    location: { x: 10, y: 20 }
};

Functions

Functions are declared using the at symbol (@).
foo = @() {
    /* do something */
};

Null

Null values can be specified using the null keyword.
x = null;

Control Structures

if/else

#'Std';
x = 2;

if (x == 1) {
    print('one');
} else if (x == 2) {
    print('two');
} else {
    print('not one or two');
}

for

#'Std';
l = [ 1, 2, 3 ];

for (x = 0; x < l.count(); x++) {
    print(l[x]);
}

for/in

#'Std';
l = [ 'a', 'b', 'c' ];

for (x in l) {
    print(x);
}

while

#'Std';
x = 0;

while (x < 5) {
    x++;
    print(x);
}

Hello, World

Getting started with Aphid is rather painless. First, add a reference to Components.Aphid.dll. Next, instantiate AphidInterpreter. Finally, invoke the instance method AphidInterpreter.Interpret to execute an Aphid script. Painless, huh? A complete C#/Aphid "Hello world" program is shown below, in listing 1.

Listing 1. A simple C#/Aphid hello world program
using Components.Aphid.Interpreter;

namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                print('Hello, world');
            ");
        }
    }
}

The C# portion of the application should be self explanatory. The Aphid program, however, warrants a bit of an explanation. The program consists of two statements.

The first is a load script statement, consisting of the load script operator (#) and the string operand, 'Std'. By default, the Aphid loader first searches the Library subdirectory of the directory in which Components.Aphid.dll resides. The loader automatically appends the ALX extension to script name passed, so in this instance it looks for <dll>\Library\Std.alx. Assuming everything is in order, it should find and load the file, which is the standard Aphid library, and contains helpful functions for manipulating strings, printing console output, etc.

The second statement is a call expression which invokes print, a function that is part of the Aphid standard library. This line of code should be rather self explanatory.

When the program is run, the output is as expected (listing 2).

Listing 2. The output a simple hello world program
Hello, world
Press any key to continue . . .

Functions

Aphid functions are defined by using the function operator (@). Since functions are first-class citizens in Aphid, they can be stored in variables (listing 3).

Listing 3. A C#/Aphid program that defines and invokes an Aphid function
using Components.Aphid.Interpreter;

namespace FunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                #'Std';

                add = @(x, y) {
                    ret x + y;
                };

                print(add(3, 7));
            ");
        }
    }
}

The output of the program is shown in listing 4.

Listing 4. The output of the function sample
10
Press any key to continue . . .

Our add function is nice, but it could be made more concise with a language feature that might be familiar to some: lambda expressions.

Lambda Expressions

Aphid lambda expressions are special functions that are formed from a single expression. When a lambda expression is invoked, the expression is evaluated and the value is returned.

Since the body of the add function from the previous example consists of a single return statement, it can be refactored into a lambda expression (listing 5).

Listing 5. A C#/Aphid program that defines and invokes an Aphid lambda expression
using Components.Aphid.Interpreter;

namespace LambdaSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                #'Std';
                add = @(x, y) x + y;
                print(add(3, 7));
            ");
        }
    }
}

The output of the program is shown in listing 6.

Listing 6. The output of the lambda sample
10
Press any key to continue . . .

Higher-order Functions

Aphid functions are values. This makes higher-order functions (i.e. functions that accept and/or return other functions) possible. Listing 7 shows a higher-order Aphid function.

Listing 7. A C#/Aphid program that defines and invokes a higher-order Aphid function
using Components.Aphid.Interpreter;

namespace HigherOrderFunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();

            interpreter.Interpret(@"
                #'Std';
                call = @(func) func();
                foo = @() print('foo() called');                
                call(foo);
            ");
        }
    }
}

The output of the program is shown in listing 8.

Listing 8. The output of the higher-order function sample
foo() called
Press any key to continue . . .

Partial Function Application

Partial function application can be used to apply arguments to a given function, producing a new function that accepts the remaining, unapplied arguments. Partial function application is performed using the function operator (@). Listing 9 shows a function named add that is partially applied with the number literal 10 to produce a new function, add10. Listing 10 shows the output of the sample program.

Listing 9. A C#/Aphid program that demonstrates partial function application
using Components.Aphid.Interpreter;

namespace PartialFunctionApplicationSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                add = @(x, y) x + y;
                add10 = @add(10);
                print(add10(20));
            ");
        }
    }
}
Listing 10. The output of the partial function application sample
30
Press any key to continue . . .

Pipelining

Pipelining, done with the pipeline operator (|>), offers an alternate syntax for calling functions. Listing 11 shows and example of pipelining, while listing 12 shows the output.

Listing 11. A C#/Aphid program that demonstrates partial function application
using Components.Aphid.Interpreter;

namespace PipeliningSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                square = @(x) x * x;
                cube = @(x) x * x * x;
                2 |> square |> cube |> print;
            ");
        }
    }
}
Listing 12. The output of the pipelining sample
64
Press any key to continue . . .

Functions that accept multiple parameters can be included in pipelines through the use of partial function application (listing 13).

Listing 13. A C#/Aphid program that demonstrates pipelining and partial function application
using Components.Aphid.Interpreter;

namespace PipeliningSample2
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                square = @(x) x * x;
                cube = @(x) x * x * x;
                add = @(x, y) x + y;
                2 |> square |> cube |> @add(4) |> print;
            ");
        }
    }
}

Extension Methods

Extension methods can be used to add methods to the built in types. This is done with the extend keyword (listing 14). When an extension method is called, the instance the method is called from is passed as the first argument (l in the example below).

Listing 14. A C#/Aphid program that demonstrates extension methods
using Components.Aphid.Interpreter;

namespace ExtensionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'std';

                extend number {
                    square: @(l) l * l
                }

                x = 10;

                print(x.square());
            ");
        }
    }
}

Of course, the programs shown so far don't really need to be scriptable. Let's take a look at some interoperability to see what we can do.

Accessing Aphid Variables from .NET

Getting and setting Aphid variables from .NET is done by accessing the CurrentScope property of an AphidInterpreter instance. CurrentScope is nothing more than an AphidObject, which is itself derived from Dictionary<string, AphidObject>. An example of getting an Aphid variable is shown in listing 15, and setting a variable is shown in listing 16.

Listing 15. An interop program that demonstrates getting an Aphid variable with C#
using Components.Aphid.Interpreter;
using System;

namespace VariableGetSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            interpreter.Interpret("x = 'foo';");
            Console.WriteLine(interpreter.CurrentScope["x"].Value);
        }
    }
}
Listing 16. An interop program that demonstrates setting an Aphid variable with C#
using Components.Aphid.Interpreter;
using System;

namespace VariableSetSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            interpreter.CurrentScope.Add("x", new AphidObject("foo"));

            interpreter.Interpret(@"
                #'Std';
                print(x);
            ");
        }
    }
}

Calling .NET Functions from Aphid scripts

Chances are, if you're reading this, you may want to expose some of your .NET functions to Aphid scripts. Doing so is quite simple. First, the .NET function of choice must be decorated with AphidInteropFunctionAttribute (listing 17). The constructor of AphidInteropFunctionAttribute accepts a string that specifies the name of the function as seen by Aphid. This can be a simple identifier (e.g. foo) or a member access expression (e.g. foo.bar.x.y). If it is the latter, Aphid will either construct an object or add members to an existing object as necessary when the function is imported.

Listing 17. An Aphid interop function
using Components.Aphid.Interpreter;

namespace InteropFunctionSample
{
    public static class AphidMath
    {
        [AphidInteropFunction("math.add")]
        public static decimal Add(decimal x, decimal y)
        {
            return x + y;
        }
    }
}

Note that the decorated function is both public and static; this is a requirement for all Aphid interop function. Now that we've created our interop function, we can proceed to write a script that imports and invokes it (listing 18).

Listing 18. An Aphid program that demonstrates loading a library and invoking an interop function
#'Std';
##'InteropFunctionSample.AphidMath';
print(math.add(3, 7));

The first line, the load script statement, was described in the Hello, world section. The second, however, is slightly different. The load library operator (##) searches Aphid modules (more on this in a bit) for the class specified by the string operand. You may recognize that the operand is the fully qualified name of the container class for the interop function we wrote previously.

So how does the Aphid interpreter know where to find InteropFunctionSample.AphidMath, you ask? Simple: we add the appropriate .NET assembly to the Aphid loader's list of modules. The relevant code is shown in listing 19.

Listing 19. An C#/Aphid program that demonstrates loading a library and invoking an interop function
using Components.Aphid.Interpreter;
using System.Reflection;

namespace InteropFunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interprer = new AphidInterpreter();
            interprer.Loader.LoadModule(Assembly.GetExecutingAssembly());
            
            interprer.Interpret(@"
                #'Std';
                ##'InteropFunctionSample.AphidMath';
                print(math.add(3, 7));
            ");
        }
    }
}

When run, the application yields the expected output (listing 20).

Listing 20. The output of the interop sample
10
Press any key to continue . . .

Now, let's flip things around.

Calling Aphid Functions from .NET

In some scenarios, you may find yourself needing to invoke Aphid functions from .NET code. This can be achieved by calling the AphidInterpreter.CallFunction instance method, which accepts a function name and the arguments to be passed (listing 21).

Listing 21. A C#/Aphid program that demonstrates calling an Aphid function from .NET
using Components.Aphid.Interpreter;
using System;

namespace CallAphidFunctionSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            interpreter.Interpret("add = @(x, y) x + y;");
            var x = interpreter.CallFunction("add", 3, 7).Value;
            Console.WriteLine(x);
        }
    }
}

When run, the Aphid function add is called (listing 22).

Listing 22. The output of the interop sample
10
Press any key to continue . . .

Object Interoperability

Some scenarios may necessitate passing objects back and forth between Aphid and .NET. This can be done by manually creating and manipulating instances of AphidObject, or by using the AphidObject.ConvertTo and AphidObject.ConvertFrom methods (listing 23). Class properties intended to be passed between Aphid and .NET via ConvertTo and ConvertFrom should be decorated with AphidPropertyAttribute.

Listing 23. A C#/Aphid program that demonstrates Aphid object interoperability
using Components.Aphid.Interpreter;
using System;

namespace ObjectSample
{
    public class Point
    {
        [AphidProperty("x")]
        public int X { get; set; }

        [AphidProperty("y")]
        public int Y { get; set; }

        public override string ToString()
        {
            return string.Format("{0}, {1}", X, Y);
        }
    }

    public class Widget
    {
        [AphidProperty("name")]
        public string Name { get; set; }

        [AphidProperty("location")]
        public Point Location { get; set; }

        public override string ToString()
        {
            return string.Format("{0} ({1})", Name, Location);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var interpreter = new AphidInterpreter();
            
            interpreter.Interpret(@"
                #'Std';
                
                ret {
                    name: 'My Widget',
                    location: { x: 10, y: 20 }
                };
            ");

            var widget = interpreter.GetReturnValue().ConvertTo<pWidget>();
            Console.WriteLine(widget);
            widget.Location.X = 40;
            var aphidWidget = AphidObject.ConvertFrom(widget);
            interpreter.CurrentScope.Add("w", aphidWidget);
            interpreter.Interpret(@"printf('New X value: {0}', w.location.x);");
        }
    }
}
Listing 24. The output of the interop sample
My Widget (10, 20)
New X value: 40
Press any key to continue . . .

Resources

History

  • 11/28/2013 - Several updates and fixes to code
    • Added switch support
    • Added range operator
    • Added conditional operator
    • Added query operators
    • Added prefix/postfix increment/decrement support
    • Added num function
    • Added env.processes function
    • Added UDP library
    • Added ILWeave tool
    • Added WPF AphidRepl Control
    • Several updates to REPL
    • Replaced exists operator with defined keyword
    • Fixed number literal tokenization issue
    • Fixed AphidObject.ConvertFrom number conversion bug
    • Fixed loader issues
    • Fixed string.substring extension method
  • 11/07/2013 - Updated article to cover control structures, partial function application, pipelining, extension methods, and object interoperability
  • 11/06/2013 - Added try/catch/finally support, added while loop support, added * assignment (+=, -=, etc) support, fixed serialization, added interop functions, fixed negative number literal support
  • 11/05/2013 - Added examples for basic types, added download link
  • 10/16/2013 - First version of this article

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

About the Author

John_Leitch
Software Developer AutoSec Tools
United States United States
No Biography provided

Comments and Discussions

 
QuestionANTLR PinmemberRicardo borges18-Jul-14 3:50 
AnswerRe: ANTLR PinprofessionalJohn_Leitch21-Jul-14 10:33 
QuestionConvert string to integer? Pinmemberrubem23-Nov-13 8:00 
AnswerRe: Convert string to integer? PinprofessionalJohn_Leitch23-Nov-13 11:30 
GeneralMy vote of 5 PinprofessionalPrasad Khandekar5-Nov-13 23:26 
GeneralRe: My vote of 5 PinprofessionalJohn_Leitch6-Nov-13 16:13 
QuestionImpressive. Thanks Pinmemberrubem28-Oct-13 11:28 
AnswerRe: Impressive. Thanks PinprofessionalJohn_Leitch28-Oct-13 13:11 
GeneralRe: Impressive. Thanks Pinmemberrubem31-Oct-13 1:36 
GeneralRe: Impressive. Thanks PinprofessionalJohn_Leitch31-Oct-13 9:26 
GeneralRe: Impressive. Thanks Pinmemberrubem1-Nov-13 8:51 
QuestionScripting accessing objects PinmemberMember 395255422-Oct-13 3:43 
AnswerRe: Scripting accessing objects PinprofessionalJohn_Leitch22-Oct-13 8:34 
QuestionRe: Scripting accessing objects PinmemberAntonio Nakić Alfirević6-Nov-13 1:37 
AnswerRe: Scripting accessing objects PinprofessionalJohn_Leitch6-Nov-13 16:14 
QuestionOther scripting tools PinmemberMember 395255421-Oct-13 19:16 
AnswerRe: Other scripting tools PinprofessionalJohn_Leitch21-Oct-13 23:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 29 Nov 2013
Article Copyright 2013 by John_Leitch
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid