How can I make my application scriptable in C#?
Asked Answered
D

7

11

I have a desktop application written in C# I'd like to make scriptable on C#/VB. Ideally, the user would open a side pane and write things like

foreach (var item in myApplication.Items)
   item.DoSomething();

Having syntax highlighting and code completion would be awesome, but I could live without it. I would not want to require users to have Visual Studio 2010 installed.

I am thinking about invoking the compiler, loading and running the output assembly.

Is there a better way?

Is Microsoft.CSharp the answer?

Disapproval answered 2/3, 2010 at 23:59 Comment(4)
It would help if you describe what exactly you want to achieve.Samphire
Wait, are you asking, "How can I make my C# application scriptable in a scripting language?" or "How can I make my C# application scriptable in C#?"Youngman
C# application scriptable in C#Disapproval
Related: #2403489Mathildemathis
Y
4

Have you thought about IronPython or IronRuby?

Youngman answered 3/3, 2010 at 0:2 Comment(4)
Performance is something you might need to monitor. +1 for the answer thoughSamphire
It depends on how much program logic is in the scriping part and how much of the heavy lifting is .NET/C# code. Python is commonly used to make large, high performance C++ programs scriptable (e.g. video games, Pixar's internal Menv animation software, etc.) but Python is not the conventional way of building new features but rather Python would allow a higher-level way of putting the existing high performance pieces together in interesting, novel ways.Youngman
Or IronJS? a little more accessible to a C# dude.Itagaki
@Sky Sanders: Didn't know about IronJS. Sounds really cool and I hope they can bring it into a stable release.Youngman
W
1

Use a scripting language. Tcl, LUA or even JavaScript comes to mind.

Using Tcl is really easy:

using System.Runtime.InteropServices;
using System;

namespace TclWrap {
    public class TclAPI {
         [DllImport("tcl84.DLL")]
         public static extern IntPtr Tcl_CreateInterp();
         [DllImport("tcl84.Dll")]
         public static extern int Tcl_Eval(IntPtr interp,string skript);
         [DllImport("tcl84.Dll")]
         public static extern IntPtr Tcl_GetObjResult(IntPtr interp);
         [DllImport("tcl84.Dll")]
         public static extern string Tcl_GetStringFromObj(IntPtr tclObj,IntPtr length);
    }
    public class TclInterpreter {
        private IntPtr interp;
        public TclInterpreter() {
            interp = TclAPI.Tcl_CreateInterp();
            if (interp == IntPtr.Zero) {
                throw new SystemException("can not initialize Tcl interpreter");
            }
        }
        public int evalScript(string script) {
            return TclAPI.Tcl_Eval(interp,script);        
        }
        public string Result {
            get { 
                IntPtr obj = TclAPI.Tcl_GetObjResult(interp);
                if (obj == IntPtr.Zero) {
                    return "";
                } else {
                    return TclAPI.Tcl_GetStringFromObj(obj,IntPtr.Zero);
                }
            }
        }
    }
}

Then use it like:

TclInterpreter interp = new TclInterpreter();
string result;
if (interp.evalScript("set a 3; {exp $a + 2}")) {
    result = interp.Result;
}
Wilhelminawilhelmine answered 3/3, 2010 at 0:2 Comment(1)
Are there any C# bridges for any of those scripting languages and if so can you provide links?Youngman
I
1

You will invoke the compiler anyway, because C# is a compiled language. The best way to do it can be checked in CSharpCodeProvider - класс.

Inform answered 3/3, 2010 at 0:3 Comment(2)
You will not invoke the compiler at runtime. You will invoke the clr however.Wilhelminawilhelmine
you will invoke compiler. csc.exe will be called anyway, you can use reflector to check thatInform
C
1

I would use PowerShell or MEF. It really depends on what you mean by scritable and what type of application you have. The best part about PowerShell is it's directly hostable and directly designed to use .NET interfaces in a scripting manner.

Conjoin answered 3/3, 2010 at 0:4 Comment(0)
G
1

You can use the following open source solution as an example: https://bitbucket.org/jlyonsmith/coderunner/wiki/Home

Giraudoux answered 3/3, 2010 at 0:58 Comment(1)
CodePlex is shutting down soon, so you may consider doing something before the link becomes broken.Mathildemathis
B
1

I had the exact same problem and with a bit of googling and few modifications I solved it using Microsoft.CSharp.CSharpCodeProvider which allows the user to edit a C# template I present to them that exposes the complete Object Model of my application and they can even pass parameters from / and return result to the application itself.

The full C# solution can be downloaded from http://qurancode.com. But here is the main code that does just that:

using System;
using System.Text;
using System.IO;
using System.Collections.Generic;
using System.Reflection;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Security;
using Model; // this is my application Model with my own classes


public static class ScriptRunner
{
    private static string s_scripts_directory = "Scripts";
    static ScriptRunner()
    {
        if (!Directory.Exists(s_scripts_directory))
        {
            Directory.CreateDirectory(s_scripts_directory);
        }
    }

    /// <summary>
    /// Load a C# script fie
    /// </summary>
    /// <param name="filename">file to load</param>
    /// <returns>file content</returns>
    public static string LoadScript(string filename)
    {
        StringBuilder str = new StringBuilder();
        string path = s_scripts_directory + "/" + filename;
        if (File.Exists(filename))
        {
            using (StreamReader reader = File.OpenText(path))
            {
                string line = "";
                while ((line = reader.ReadLine()) != null)
                {
                    str.AppendLine(line);
                }
            }
        }
        return str.ToString();
    }

    /// <summary>
    /// Compiles the source_code 
    /// </summary>
    /// <param name="source_code">source_code must implements IScript interface</param>
    /// <returns>compiled Assembly</returns>
    public static CompilerResults CompileCode(string source_code)
    {
        CSharpCodeProvider provider = new CSharpCodeProvider();

        CompilerParameters options = new CompilerParameters();
        options.GenerateExecutable = false;  // generate a Class Library assembly
        options.GenerateInMemory = true;     // so we don;t have to delete it from disk

        Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
        foreach (Assembly assembly in assemblies)
        {
            options.ReferencedAssemblies.Add(assembly.Location);
        }

        return provider.CompileAssemblyFromSource(options, source_code);
    }

    /// <summary>
    /// Execute the IScriptRunner.Run method in the compiled_assembly
    /// </summary>
    /// <param name="compiled_assembly">compiled assembly</param>
    /// <param name="args">method arguments</param>
    /// <returns>object returned</returns>
    public static object Run(Assembly compiled_assembly, object[] args, PermissionSet permission_set)
    {
        if (compiled_assembly != null)
        {
            // security is not implemented yet !NIY
            // using Utilties.PrivateStorage was can save but not diaplay in Notepad
            // plus the output is saved in C:\Users\<user>\AppData\Local\IsolatedStorage\...
            // no contral over where to save make QuranCode unportable applicaton, which is a no no
            //// restrict code security
            //permission_set.PermitOnly();

            foreach (Type type in compiled_assembly.GetExportedTypes())
            {
                foreach (Type interface_type in type.GetInterfaces())
                {
                    if (interface_type == typeof(IScriptRunner))
                    {
                        ConstructorInfo constructor = type.GetConstructor(System.Type.EmptyTypes);
                        if ((constructor != null) && (constructor.IsPublic))
                        {
                            // construct object using default constructor
                            IScriptRunner obj = constructor.Invoke(null) as IScriptRunner;
                            if (obj != null)
                            {
                                return obj.Run(args);
                            }
                            else
                            {
                                throw new Exception("Invalid C# code!");
                            }
                        }
                        else
                        {
                            throw new Exception("No default constructor was found!");
                        }
                    }
                    else
                    {
                        throw new Exception("IScriptRunner is not implemented!");
                    }
                }
            }

            // revert security restrictions
            //CodeAccessPermission.RevertPermitOnly();
        }
        return null;
    }

    /// <summary>
    /// Execute a public static method_name(args) in compiled_assembly
    /// </summary>
    /// <param name="compiled_assembly">compiled assembly</param>
    /// <param name="methode_name">method to execute</param>
    /// <param name="args">method arguments</param>
    /// <returns>method execution result</returns>
    public static object ExecuteStaticMethod(Assembly compiled_assembly, string methode_name, object[] args)
    {
        if (compiled_assembly != null)
        {
            foreach (Type type in compiled_assembly.GetTypes())
            {
                foreach (MethodInfo method in type.GetMethods())
                {
                    if (method.Name == methode_name)
                    {
                        if ((method != null) && (method.IsPublic) && (method.IsStatic))
                        {
                            return method.Invoke(null, args);
                        }
                        else
                        {
                            throw new Exception("Cannot invoke method :" + methode_name);
                        }
                    }
                }
            }
        }
        return null;
    }

    /// <summary>
    /// Execute a public method_name(args) in compiled_assembly
    /// </summary>
    /// <param name="compiled_assembly">compiled assembly</param>
    /// <param name="methode_name">method to execute</param>
    /// <param name="args">method arguments</param>
    /// <returns>method execution result</returns>
    public static object ExecuteInstanceMethod(Assembly compiled_assembly, string methode_name, object[] args)
    {
        if (compiled_assembly != null)
        {
            foreach (Type type in compiled_assembly.GetTypes())
            {
                foreach (MethodInfo method in type.GetMethods())
                {
                    if (method.Name == methode_name)
                    {
                        if ((method != null) && (method.IsPublic))
                        {
                            object obj = Activator.CreateInstance(type, null);
                            return method.Invoke(obj, args);
                        }
                        else
                        {
                            throw new Exception("Cannot invoke method :" + methode_name);
                        }
                    }
                }
            }
        }
        return null;
    }
}

I then defined a C# Interface to be implemented by the user code where they are free to put anythng they like inside their concrete Run method:

/// <summary>
/// Generic method runner takes any number and type of args and return any type
/// </summary>
public interface IScriptRunner
{
    object Run(object[] args);
}

And here is the startup template the user can extends:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Text;
using System.IO;
using Model;

public class MyScript : IScriptRunner
{
    private string m_scripts_directory = "Scripts";

    /// <summary>
    /// Run implements IScriptRunner interface
    /// to be invoked by QuranCode application
    /// with Client, current Selection.Verses, and extra data
    /// </summary>
    /// <param name="args">any number and type of arguments</param>
    /// <returns>return any type</returns>
    public object Run(object[] args)
    {
        try
        {
            if (args.Length == 3)   // ScriptMethod(Client, List<Verse>, string)
            {
                Client client = args[0] as Client;
                List<Verse> verses = args[1] as List<Verse>;
                string extra = args[2].ToString();
                if ((client != null) && (verses != null))
                {
                    return MyMethod(client, verses, extra);
                }
            }
            return null;
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, Application.ProductName);
            return null;
        }
    }

    /// <summary>
    /// Write your C# script insde this method.
    /// Don't change its name or parameters
    /// </summary>
    /// <param name="client">Client object holding a reference to the currently selected Book object in TextMode (eg Simplified29)</param>
    /// <param name="verses">Verses of the currently selected Chapter/Page/Station/Part/Group/Quarter/Bowing part of the Book</param>
    /// <param name="extra">any user parameter in the TextBox next to the EXE button (ex Frequency, LettersToJump, DigitSum target, etc)</param>
    /// <returns>true to disply back in QuranCode matching verses. false to keep script window open</returns>
    private long MyMethod(Client client, List<Verse> verses, string extra)
    {
        if (client == null) return false;
        if (verses == null) return false;
        if (verses.Count == 0) return false;

        int target;
        if (extra == "")
        {
            target = 0;
        }
        else
        {
            if (!int.TryParse(extra, out target))
            {
                return false;
            }
        }

        try
        {
            long total_value = 0L;
            foreach (Verse verse in verses)
            {
                total_value += Client.CalculateValue(verse.Text);
            }
            return total_value;
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, Application.ProductName);
            return 0L;
        }
    }
}

And this is how I call it from my MainForm.cs

#region Usage from MainForm
if (!ScriptTextBox.Visible)
{
    ScriptTextBox.Text = ScriptRunner.LoadScript(@"Scripts\Template.cs");
    ScriptTextBox.Visible = true;
}
else // if visible
{
    string source_code = ScriptTextBox.Text;
    if (source_code.Length > 0)
    {
        Assembly compiled_assembly = ScriptRunner.CompileCode(source_code);
        if (compiled_assembly != null)
        {
            object[] args = new object[] { m_client, m_client.Selection.Verses, "19" };
            object result = ScriptRunner.Run(compiled_assembly, args);
            // process result here
        }
    }
    ScriptTextBox.Visible = false;
}
#endregion

Still to do is the Syntax Highlighting and CodeCompletion though.

Good luck!

Bocock answered 30/1, 2013 at 6:27 Comment(2)
Re "exposes the complete Object Model of my application": What is the object model? Is it all (public) classes with their public methods/properties, or what?Mathildemathis
Yes, that is what I meant. Class model would've been better choice but uncommon :)Bocock
S
0

What language is your application written in? If C++, you might consider Google V8, an embeddable ECMAScript/JavaScript engine.

Schottische answered 3/3, 2010 at 0:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.