DEV Community

Jochem Stoel
Jochem Stoel

Posted on

V8Ception | How to implement V8 into your .NET applications.

This article is based on my old V8 article published on 23 august 2015.

V8 is the JavaScript execution engine built for Google Chrome and open-sourced by Google in 2008. Written in C++, V8 compiles JavaScript source code to native machine code instead of interpreting it in real time.

In this post I will explain how to

  • install the required dependencies from NuGet
  • create instances of V8 script engine inside your C# .NET application.
  • evaluate JavaScript input
  • expose a host type to the V8 interpreter context
  • expose a host object to the V8 interpreter context
  • expose entire assemblies to your script context
  • create a host object and call methods from script

Provide examples (with gists) that show how to

  • write a super simple REPL application
  • create an easy way to load JavaScript files
  • let V8 reference itself (both type and instance)

Google V8 Engine

This post is written with n00bs in mind but assumes that you have some basic idea of what you're doing. I have simplified the concepts introduced in this article to the best of my ability and I have made all the code snippets available on my GitHub gists.

Project

Start Microsoft Visual Studio and create a new C# application. (Console or Winforms does not matter) Right click the project references and click Manage nuGet packages. Enable/install Clearscript.V8 and then close the window.

Add NuGet package

Make sure to select the right package (ClearScript.V8).

Microsoft.ClearScript.V8

ClearScript is a library that makes it easy to add scripting to your .NET applications. It currently supports JavaScript (via V8 and JScript) and VBScript. Basically, ClearScript assigns application objects such as Console, File and even Winforms components to Javascript objects. You can assign both object instances and object types to a script context.

Add reference to Microsoft.Clearscript.v8



using Microsoft.ClearScript.V8 


Enter fullscreen mode Exit fullscreen mode

You can create a new instance of V8 like this



V8ScriptEngine v8 = new V8ScriptEngine();


Enter fullscreen mode Exit fullscreen mode

Adding an object instance allows you to control (an already created) object instance from your script. If you assign the system Console (System.Console) to myConsole, you will be able to access the console like this



myConsole.ReadKey(); /* Read single key input from user */
myConsole.WriteLine(); /* Write something to the console */


Enter fullscreen mode Exit fullscreen mode

So. If your Winforms application has a Button, lets say Button1 then you assign it to the V8 context like this



v8.AddHostObject("btn1", Button1); // variable btn1 reflects Button1


Enter fullscreen mode Exit fullscreen mode

and then from your script you can change the value of the button simply by doing



btn1.Text = "exit now"; /* The button text is now changed to "exit now" */


Enter fullscreen mode Exit fullscreen mode

An object type is (obviously) a reference to the type of application object (in other words, the class) rather than an instance of it. An object type is not instantiated yet.



/* Inside your application */
v8.AddHostType("Button", typeof(Button));


Enter fullscreen mode Exit fullscreen mode

and



/* Inside your script you create multiple instances of Button */
var button1 = new Button();
var button2 = new Button();
var button3 = new Button();


Enter fullscreen mode Exit fullscreen mode

If the difference between host object and object types is not clear by now then you are not ready to be using v8 in your applications.

The ClearScript package also allows you to expose an entire namespace all at once. The HostTypeCollection constructor takes 1 or more namespace (string) arguments. HostTypeCollection is located in Microsoft.ClearScript so besides Microsoft.ClearScript.V8 you need to also reference Microsoft.ClearScript. This can be useful if you want to import/access a lot of different stuff that you don't want to add manually but can also be used when you are dynamically/programatically loading .DLL files.



v8.AddHostObject(identifier, new HostTypeCollection(namespaces[]));


Enter fullscreen mode Exit fullscreen mode


/* expose entire assemblies */
    engine.AddHostObject("lib", new HostTypeCollection("mscorlib", "System.Core"));
    engine.Execute("console.log(lib.System.DateTime.Now)");
// of course assuming console.log is already implemented


Enter fullscreen mode Exit fullscreen mode

Example 1 | A simple REPL using V8

Similar to Node, a super simple REPL reads input from process stdin and evaluates it.



using System;
using Microsoft.ClearScript.V8;

namespace v8repl
{
    class Program
    {
        static void Main(string[] args)
        {
            /* create instance of V8 */
            V8ScriptEngine v8 = new V8ScriptEngine();
            /* assign System.Console to Javascript variable myConsole */
            v8.AddHostType("myConsole", typeof(Console));

            /* */
            bool kill = false; 

            /* keep doing the following while kill = false */
            while(!kill)
            {
                /* get input string from process stdin */
                string input = Console.ReadLine();

                /* using a string literal for simplicity sake */
                if(input == "exit")
                {
                    Environment.Exit(0); /* exit code 0 means no error */
                }

                /* safely evaluate input in a try/catch block */
                try
                {
                    v8.Evaluate(input); /* run the code */
                } catch (Exception e)
                {
                    /* something went wrong, show us the exception */
                    Console.WriteLine(e.Message);
                }
            }
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Gist

Example 2 | REPL 'wrapper' Class / load files

Simple class that wraps V8 and adds a method to load a file from the disk. This is not the ultimate way to design a REPL but is fine for this example.



using System;
using Microsoft.ClearScript;
using Microsoft.ClearScript.V8;

namespace v8repl
{
    class REPL
    {
        /* v8 engine outside main loop */
        private V8ScriptEngine v8 = new V8ScriptEngine();
        private bool running = false; /* */

        /* keep reading input from stdin until running = false */
        public void Start()
        {
            running = true;
            while (running)
            {
                string line = Console.ReadLine();

                if (line.Equals("kill"))
                {
                    running = false; /* causes this while loop to stop */
                }
                else {
                    Run(line);
                }
            }
        }

        /* method to read and evaluate JavaScript file */
        public void LoadFile(string inputFile)
        {
            v8.Evaluate(
                System.IO.File.ReadAllText(inputFile)
            );
        }

        /* safely evaluate code like we did before */
        public void Run(string line)
        {
            try
            {
                v8.Evaluate(line);
            }
            catch (System.Exception e)
            {
                Console.Error.WriteLine(e.Message);
            }
        }

        /* this allows us to get current instance */
        public V8ScriptEngine GetInstance()
        {
            return v8;
        }
    }
}


Enter fullscreen mode Exit fullscreen mode

Gist

Example 3 | initialization script

Using the REPL class above, we load this file init.js that contains a simple console object to sort of mimic the standarized JavaScript console object.

Application


using System;
using Microsoft.ClearScript.V8;
using Microsoft.ClearScript;

namespace v8repl
{
    class Program
    {
        static void Main(string[] args)
        {
            var repl = new REPL();
            var v8 = repl.GetInstance(); // shortcut 

            /* assign the whole .NET core library to mscorlib */
            v8.AddHostObject("mscorlib", new HostTypeCollection("mscorlib"));

            /* reference full namespace, for example:
             * mscorlib.System.Console.WriteLine()
             * mscorlib.System.IO.File.WriteAllText()
             */

            /* expose the V8ScriptEngine type to the V8 context */
            v8.AddHostType("V8ScriptEngine", typeof(V8ScriptEngine));

            /* we can now do:
             * var context1 = new V8ScriptEngine()
             */

            repl.LoadFile("init.js"); /* evaluate our file init.js */
            repl.Start();
        }
    }
}


Enter fullscreen mode Exit fullscreen mode
The JavaScript file init.js being loaded


/* imitate standarized console object */
var console = {
    log: string => {
        mscorlib.System.Console.WriteLine(string)
    },
    error: string => {
        mscorlib.System.Console.Error.WriteLine(string)
    }
}

/* 
Mental note:

In JavaScript we can pass multiple variables to console.log doing console.log('string', someVariable, 1234, { a: 1 })
This will not work here because Console.WriteLine expects a string.
You need some logic that will Array.join() the function arguments.
*/


Enter fullscreen mode Exit fullscreen mode

Gist

V8ception | V8 inside V8

You can even assign the V8ScriptEngine itself to a Javascript object.



v8.AddHostType("V8Engine", typeof(V8ScriptEngine));


Enter fullscreen mode Exit fullscreen mode

Now you can create a new instance of V8 from your application input script (JavaScript). This allows you to create a new (sub)context. A new subcontext means a new scope / execution environment without any inherited variables.



/* Javascript */
var v8 = new V8Engine();
v8.Execute('var x = 3; var y = 5; var z = x+y;');


Enter fullscreen mode Exit fullscreen mode

Note that this V8 instance will also need its own console if you want to print its values to your process stdout.

Self reference

You can create a reference to the V8 instance itself. This is a terrible idea no matter what the circumstances. :P



V8ScriptEngine v8 = new V8ScriptEngine();
v8.AddHostObject("self", v8);

Enter fullscreen mode Exit fullscreen mode


self.Evaluate("var name = 'Jochem'")
console.log(name) // this will work

Enter fullscreen mode Exit fullscreen mode


var myConsole = {
log: function() {
//
}
}

self.AddHostObject('parentConsole', myConsole) // sketchy but works

Enter fullscreen mode Exit fullscreen mode




One More Thing

It is important to understand that V8 is only the interpreter. A lot of standard and very common objects/apis that you know do not exist yet. This includes the console object like we have discussed in this article but also the Event class and its children.



To learn more about ClearScript, I highly recommend checking out the official documentation on CodePlex, this is where I started too.

V8Ception

Top comments (12)

Collapse
 
bugmagnet profile image
Bruce Axtens • Edited

Above you say, "You can create a reference to the V8 instance itself. This is a terrible idea no matter what the circumstances.".

In my project (which uses ClearScript to wrap V8) I have

    private static void SetupIncludeFunction()
    {
        v8.AddHostObject("CSV8", v8);
        var includeCode = @"
        function include(fn) {
            if (CSFile.Exists(fn)) {
                CSV8.Evaluate(CSFile.ReadAllText(fn));
            } else {
                throw 'File not found.';
            }
        }";

        v8.Evaluate(includeCode);
    }

which by definition is "not a good thing."

Seeing as I want to have an include function (and having it in such as form as to be able to be put inside at try/catch), how should I have done it?

Collapse
 
jochemstoel profile image
Jochem Stoel

It is a long time ago I did all this but let me see. Ignoring most dogmas, What you're doing here looks about right.

A terrible idea is to expose the internal instance of the interpreter to the higher context using addHostObject. It will stay there in that (more) global context after the first call to include. Simply wrap that reference too somewhere or safely delete it afterwards, otherwise

include 'file.js' // CSV8 is now available
CSV8.DestroySystem()

I don't know from memory how V8 responds to setting a host object twice but maybe your code will crash if you make a second call to include. (maaaaybe)

Make sure that you can rely on CSFile.Exists because if it is a direct reference to System.IO.File.Exists which expects a String, ...

include({ crash: 'boom!' }) // this was not expected!

You can wrap it somewhere else but I advise to be lazy like me and put the call to CSFile.Exists it inside a try block of include(). Again, only if needed. I don't know the rest of your program.

Maybe have a look at NiL. I never really liked the extra .dll files of v8, also didn't feel like embedding them or building ClearScript every time I test my code. NiL.js is an ECMAScript evaluator written in C# that understands modern ES6/7/8/9/10 and takes only a second to compile. Not that compiling is needed. It also provides APIs to extend the evaluator with your custom syntax which allowed me to implement an include method function keyword natively into the interpreter. Not for beginners though. In the documentation are some examples, they show you how to add an echo keyword or something. It is platform agnostic in the sense that it runs on Mono.

Collapse
 
bugmagnet profile image
Bruce Axtens

They've updated their examples a bit since you were last there. There are custom statements and custom operators at github.com/nilproject/NiL.JS/tree/...

Thread Thread
 
jochemstoel profile image
Jochem Stoel

What are you working on?

Thread Thread
 
bugmagnet profile image
Bruce Axtens

When I get the readme done and a folder with a couple of samples in, I'll send the link. In the meantime, I've got a an enhanced JavaScript called Lychen ("Lichen" is already taken). For some years my work has been using an enhanced JScript using the same technique. I'm now pursuing V8 as a personal project.

Thread Thread
 
bugmagnet profile image
Bruce Axtens • Edited

Project now on GitHub in a public repo at github.com/axtens/Lychen. Other repos have been deleted.

Thread Thread
 
jochemstoel profile image
Jochem Stoel

Hey Bruce, it's been some time since you commented on this article. I went to check out Lychen (again) on GitHub today and noticed your last commit was 29 days ago. May I ask, what do you use Lychen for? Why can't you do it with regular JavaScript? What is the use case? Anyone else using it as far as you know? Cyanobacteria sure but what is the objective of this project? :)

Thread Thread
 
bugmagnet profile image
Bruce Axtens • Edited

As far as I know, no one is using it. I use it as a test-bed for ideas which I then propagate to our in-house tool, which also uses ClearScript but targets JScript rather than V8. I've recently started fiddling with LychenBASIC, which targets the VBScript engine via ClearScript.

In some respects I work in a backwater. We have a commitment to the Windows platform (which I chafe against from time to time): Azure, SQL Server, C# with JavaScript as the extension language, Visual Studio and Visual Studio Code (when not using Notepad++).

As for "why can't you do it with regular JavaScript?" well-l-l-l, okay, maybe I'll start learning nodejs. Maybe there's some way of using it in our current ecosystem.

Collapse
 
rjamesnw profile image
James

FYI: ClearScript doesn't yet support .Net Core, but V8.Net now does: nuget.org/packages/V8.Net/

Collapse
 
jochemstoel profile image
Jochem Stoel

FYI: Maybe not out of the box but it is really not that hard getting it to work (ish).

Collapse
 
akashkava profile image
Akash Kava

As per Clear Script authors, github.com/Microsoft/ClearScript/i... , ClearScript is written with Mixed Assemblies which is not supported on any other platform except Windows. So it is not that easy.

Thread Thread
 
jochemstoel profile image
Jochem Stoel

Thank you for clarifying but this article is a rewrite of an even older article targeted at Windows from the beginning. When this was written a couple years ago there was no support for .NET on Linux besides Mono. All the examples assume Windows and even show screenshots of Visual Studio running on Windows so someone who wants to implement V8 into a Linux application really should read a different article and I see no point of dragging this out.