Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / F#

Embedded Scripting using F#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
22 Aug 2011CPOL6 min read 52K   1K   19   9
This article shows how to compile and run F# code during runtime.

Introduction

In this article, I describe how to extend an application functionality with F# scripting. Sometimes, there is a need to extend existing application functionality with some custom behavior. If you don't want or can't go through application developing and building cycle every time you need an additional feature, scripting can be a good solution. It allows to tailor your application for particular needs with minimal efforts.

Since F# is a fully supported language in .NET, it is easy and natural to incorporate F# scripting in .NET applications. I will show you how to add F# scripting to Windows Forms application.

The accompanying sample application has two major parts:

  1. Host program written in C#. This simple Windows Forms application is built in FSCompileTestForm project.
  2. Embedded extension written in F# and designed as a class library. It is built in FSExecutor project.

The whole application is built against .NET Framework 4.0. Used F# compiler is taken from F# PowerPack package.

Example of running simple script with complex numbers arithmetic is shown in the following screenshot:

Sample application screenshot.

Preparations

Embedded script needs to exchange data with host application. It can be achieved by using common shared components or by data streams. There are standard input/output/error streams. It would be convenient to leverage them in scripting program using simple printfn functions. In the sample program, standard output and error streams are redirected to the bottom text area. This functionality is implemented in StdStreamRedirector module. This module has static method Init which accepts two actions and returns Redirector object. Redirector object implements IDisposable interface. When it is disposed, native stream handles are returned to its original state.

F#
let Init(stdOutAction: Action<string>, stdErrAction: Action<string>) = 
    new Redirector(stdOutAction, stdErrAction)

Provided actions are used to handle data coming from standard output and standard error streams correspondingly. These actions can do anything with the provided data. It depends on their implementations. I chose to put incoming data into text area using black color for stdout data and red color for stderr data.

Standard streams in .NET applications work as if they are initialized during the first use and stay in this initialized state all the time. So standard stream initialization should be done as soon as possible before any use of Console.Out or Console.Error streams or any printf/eprintf function calls in script. I put this code into main form's Load event callback.

F#
private void MainForm_Load(object sender, EventArgs e)
{
    Action<string> aOut = (text => AppendStdStreamText(text, Color.Black));
    Action<string> aErr = (text => AppendStdStreamText(text, Color.DarkRed));
    m_r = StdStreamRedirector.Init(aOut, aErr);
}

Redirector object m_r should be disposed after .NET standard streams are finished to use. So its disposal is placed into main form's Dispose method:

F#
protected override void Dispose(bool disposing)
{
    if (disposing && (components != null))
    {
        ((System.IDisposable)m_r).Dispose();
        m_r = null;

        components.Dispose();
    }
    base.Dispose(disposing);
}

Redirector object are implemented in F# and F# always implements interfaces explicitly, so to be able to call Dispose() method of Redirector object, it must be explicitly cast to IDisposable interface.

Internally Redirector object uses AnonymousPipeServerStream objects connected to standard streams. For this purpose, SetStdHandle and GetStdHandle functions from Kernel32.dll are used.

F#
module private Win32Interop = 
    [<DllImport("Kernel32.dll")>]
    extern [<marshalas(unmanagedtype.bool)>] 
		bool SetStdHandle(UInt32 nStdHandle, IntPtr hHandle)
    [<DllImport("Kernel32.dll")>]
    extern IntPtr GetStdHandle(UInt32 nStdHandle)

type Redirector(out: Action<string>, err: Action<string>) = 
    let stdOutHandleId = uint32(-11)
    let stdErrHandleId = uint32(-12)
    let pipeServerOut = new AnonymousPipeServerStream(PipeDirection.Out)
    let pipeServerErr = new AnonymousPipeServerStream(PipeDirection.Out)

    do if not(Win32Interop.SetStdHandle
	(stdOutHandleId, pipeServerOut.SafePipeHandle.DangerousGetHandle())) 
        then failwith "Cannot set handle for stdout."
    do if not(Win32Interop.SetStdHandle
	(stdErrHandleId, pipeServerErr.SafePipeHandle.DangerousGetHandle())) 
        then failwith "Cannot set handle for stderr."
    do if not(ThreadPool.QueueUserWorkItem
	(fun o -> readPipe(pipeServerOut.ClientSafePipeHandle, out))) 
        then failwith "Cannot run listner thread."
    do if not(ThreadPool.QueueUserWorkItem
	(fun o -> readPipe(pipeServerErr.ClientSafePipeHandle, err))) 
        then failwith "Cannot run listner thread."

Two independent threads from application thread pool read data from pipes connected to standard streams and send data to provided actions. For simplicity, all exceptions are ignored inside these threads.

F#
let private readPipe (h: SafePipeHandle, a: Action<string>) = 
    use clientPipeStream = new AnonymousPipeClientStream(PipeDirection.In, h)
    use reader = new StreamReader(clientPipeStream)
    try
        while not(reader.EndOfStream) do
            let s = reader.ReadLine()
            a.Invoke(s)
    with
        | ex -> ()

This implementation makes output and error streams totally independent and, taking into account latency of thread pool threads, it is possible that output of printf/eprintf can be unordered. Data from thread pool threads are coming into Windows Forms text area which belongs to GUI thread. But actions called in thread pool cannot directly access GUI components, they must do it through Contorl.Invoke method in GUI thread. First, better to check if the Invoke call is actually required by checking InvokeRequired property. And if it is required, then the appending colored text is called using anonymous delegate through Invoke method. Invoke method and InvokeRequired property are thread safe, so it is OK to call them from thread pool threads. No additional synchronization is required.

F#
private void AppendStdStreamText(string text, Color c)
{
    if(this.InvokeRequired)
    {
        MethodInvoker del = delegate
        {
            AppendStdStreamText(text, c);
        };
        this.Invoke(del);
        return;
    }
    AppendColoredText(text, c);
    m_outputTB.AppendText("\n");
}
private void AppendColoredText(string text, Color c)
{
    int l1 = m_outputTB.TextLength;
    m_outputTB.AppendText(text);
    int l2 = m_outputTB.TextLength;
    m_outputTB.SelectionStart = l1;
    m_outputTB.SelectionLength = l2 - l1;
    m_outputTB.SelectionColor = c;
}

Method AppendColoredText adds provided text to the control and sets required color to it.

Here is the screenshot for different stream colored text:

Colored standard output and error streams.

Script Compilation and Execution

Now, when all preparations are finished, it's time to compile script text into executable assembly. For this purpose, F# compiler from F# PowerPack package is used. All compilation and execution functionality is implemented in FSExecutor module. For compilation, script code in a single string and list of referenced assemblies in seq<string>(F#) which corresponds to IEnumerable<string>(C#) are provided. Referenced assemblies can be set using absolute assembly paths or just DLL names for GAC registered assemblies.

First F# compiler object must be created. Then different compilation parameters are set to the CompilerParameters object. Sequence of referenced assemblies is also set to the compiler parameters.

Since script is usually not a big program, the resulting assembly is generated in memory to accelerate the whole process, so GenerateInMemory property is set to true. Script program is not a class library and should have single EntryPoint property of MethodInfo type, so GenerateExecutable property should be set to true as well.

F#
let compile (code: string) references = 
    let compiler = new FSharpCodeProvider()
    let cp = new System.CodeDom.Compiler.CompilerParameters()
    for r in references do cp.ReferencedAssemblies.Add(r) |> ignore done
    cp.GenerateInMemory <- true
    cp.GenerateExecutable <- true
    let cr = compiler.CompileAssemblyFromSource(cp, code)
    (cr.CompiledAssembly, cr.Output, cr.Errors)

Function compile returns tuple with compiled assembly and compilation output and error messages. If there are error messages, they are print to standard error stream and script execution is finished.

Compilation failed.

If no error messages are present, then all output messages are print to standard output stream and the generated assembly is executed.

F#
let CompileAndExecute(code: string, references: seq<string>) = 
    let sw = new Stopwatch()
    sw.Start()
    let (assembly, output, errors) = compile code references
    if errors.Count > 0 then
        for e in errors do eprintfn "%s" (e.ToString()) done
    else
        for o in output do printfn "%s" o done
        executeAssembly assembly
    sw.Stop()
    printfn "%s %i milliseconds." "Compile and execute takes" sw.ElapsedMilliseconds

Stopwatch object is used to measure the total time of compilation and execution. After script is finished, the total execution time is printed as well.

Function executeAssembly is used for actual compiled assembly execution.

F#
let executeAssembly (a: Assembly) = 
    try
        a.EntryPoint.Invoke(null, null) |> ignore
        printfn "Execution successfully completed."
    with
        | :? TargetInvocationException as tex -> eprintfn 
		"Execution failed with: %s" (tex.InnerException.Message)
        | ex -> eprintfn "Execution cannot start, reason: %s" (ex.ToString())

The EntryPoint property is used to run the script. This simplifies writing script much because it is not needed to wrap the code in a class or module.

All script's runtime errors are captured in a TargetInvacationException class and handled differently from all other errors to be able to distinguish scripting errors from the host program's errors.

Script Execution Optimization

As you can see, the first script execution takes lots of time even for tiny scripts. It takes 1713 milliseconds in the first screenshot. The reason of slowness is the compilation. F# compiler is quite intelligent and it requires some time to figure out all omitted types and perform required optimizations. However if the same script is running multiple times, it is better to compile it once and then execute as many times as needed. For this purpose, CompileAndExecute function uses CompiledAssemblies dictionary to map script code and compiled assembly. If for some provided code, the compiled assembly is found in the dictionary, then this assembly is executed and no additional compilation happens. But script can be quite long, so it is better to use not the text itself but its hash value. MD5 hash algorithm is used to compare script codes. It does not provide full proof comparison but for the most practical solutions, the probability of collision is negligibly small. Modified CompileAndExecute function with MD5 calculations is provided here:

F#
let getMd5Hash (code: string) = 
    let md5 = MD5.Create()
    let codeBytes = Encoding.UTF8.GetBytes(code)
    let hash = md5.ComputeHash(codeBytes)
    let sb = new StringBuilder()
    for b in hash do sb.Append(b.ToString("x2")) |> ignore done
    sb.ToString()

let CompileAndExecute(code: string, references: seq<string>) = 
    let sw = new Stopwatch()
    sw.Start()
    let hash = getMd5Hash code
    if CompiledAssemblies.ContainsKey(hash) then
        executeAssembly CompiledAssemblies.[hash]
    else
        let (assembly, output, errors) = compile code references
        if errors.Count > 0 then
            for e in errors do eprintfn "%s" (e.ToString()) done
        else
            for o in output do printfn "%s" o done
            executeAssembly assembly
            CompiledAssemblies.Add(hash, assembly)
    sw.Stop()
    printfn "%s %i milliseconds." "Compile and execute takes" sw.ElapsedMilliseconds

As you see in the screenshots, successive executions which use cached compiled assemblies are extremely fast. These executions take less than 1 millisecond and stopwatch reports 0 milliseconds.

Conclusions

F# scripting is extremely convenient because F# language is succinct, execution performance is pretty good and can be compared with other high performance solutions on C#, Java and sometimes C++. Also F# can be perfectly merged with any .NET environment utilizing broad variety of high quality .NET libraries.

However F# compilation itself is quite slow. If script code is changing much and execution of each code version isn't repeated, then pure dynamic languages (e.g. Python) with fast compilation stage are more suitable. Their execution performance and type safety is not so great as in F# but, considering compilation time, overall performance can be better.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
My professional carrier started in 1997. I have broad range of interests including database and web development, compiler tools and technologies. I am experienced in C++, C#/F# and Java. I am watching mobile development with great attention.

Comments and Discussions

 
QuestionAnd C++ Pin
pachesantiago22-Jul-13 1:29
pachesantiago22-Jul-13 1:29 
AnswerRe: And C++ Pin
Vladimir Ivanovskiy22-Jul-13 16:19
Vladimir Ivanovskiy22-Jul-13 16:19 
QuestionExecuting this in a clean machine Pin
smolina_741-May-12 4:15
smolina_741-May-12 4:15 
AnswerRe: Executing this in a clean machine Pin
Vladimir Ivanovskiy3-Jul-12 18:25
Vladimir Ivanovskiy3-Jul-12 18:25 
QuestionVote of 5 in vs10sp1ult on win7x64 Pin
bmac29-Aug-11 11:19
bmac29-Aug-11 11:19 
GeneralMy vote of 5 Pin
Andreas Kroll24-Aug-11 0:16
Andreas Kroll24-Aug-11 0:16 
QuestionCannot use sample Pin
Andreas Kroll24-Aug-11 0:16
Andreas Kroll24-Aug-11 0:16 
AnswerRe: Cannot use sample Pin
Vladimir Ivanovskiy24-Aug-11 2:30
Vladimir Ivanovskiy24-Aug-11 2:30 
Hi Andreas,
What kind of Windows do you use? Is it 32 or 64 platform? I have built my example on Vista 32bit.
I beleive something bad happens to your VS2010 installation because I can successfully add System.Numerics.dll into the reference list of any project.
I have System.Numerics.dll ver. 4.0.0.0 and runtime version is 4.0.30319.
Did you try to use absolute dll path in the example reference list?
GeneralRe: Cannot use sample Pin
Andreas Kroll24-Aug-11 2:41
Andreas Kroll24-Aug-11 2:41 

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

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