Building a simple Javascript REPL in C++ with V8

In the last post, we compiled Google's V8 Javascript engine. Now, that we have it compiled, we can play around with it in our C++ applications. I thought a good first test would be to build a super simple REPL like Nodejs has. Even though I am very new to C++, this is a simple example, so I was able to figure it out. Hopefully I am not doing anything completely wrong. I plan to revisit this once I have more time to brush up on my C++ skills.

The Code

Google's website has a good Hello World example for embedding V8 into your program. We can start with this, and with a little more code, we have our REPL. Here is the Hello World code straight from Google's getting started guide:

#include <v8.h>

using namespace v8;

int main(int argc, char* argv[]) {

  // Get the default Isolate created at startup.
  Isolate* isolate = Isolate::GetCurrent();

  // Create a stack-allocated handle scope.
  HandleScope handle_scope(isolate);

  // Create a new context.
  Handle<Context> context = Context::New(isolate);

  // Enter the context for compiling and running the hello world script.
  Context::Scope context_scope(context);

  // Create a string containing the JavaScript source code.
  Handle<String> source = String::NewFromUtf8(isolate, "'Hello' + ', World!'");

  // Compile the source code.
  Handle<Script> script = Script::Compile(source);

  // Run the script to get the result.
  Handle<Value> result = script->Run();

  // Convert the result to an UTF8 string and print it.
  String::Utf8Value utf8(result);
  printf("%s\n", *utf8);
  return 0;
}

This is a fairly simple example, but it gives us much information. We can use this code, and by adding a few more lines of code, we can turn it into a REPL. First, create a new C++ console application in Visual Studio.

Here is the modified version. I will list the code here, and then we can go over it piece by piece.

#include <v8.h>
#include <stdlib.h>
#include <string.h>

using namespace v8;

#define MAX_SCRIPT_LEN 512

int main(int argc, char* argv[]) {
    printf("Javascript REPL (Ctrl+C to quit)\n");

    Isolate* isolate = Isolate::GetCurrent();
    HandleScope handle_scope(isolate);
    Handle<Context> context = Context::New(isolate);
    Context::Scope context_scope(context);

    while (1) {
        char *scr = (char*)malloc(MAX_SCRIPT_LEN);

        printf("> ");
        fgets(scr, MAX_SCRIPT_LEN, stdin);

        Handle<String> source = String::NewFromUtf8(isolate, scr);
        Handle<Script> script = Script::Compile(source);
        Handle<Value> result = script->Run();
        String::Utf8Value utf8(result);

        printf("%s\n", *utf8);

        free(scr);
    }

    return 0;
}

As you can see, it doesn't take much. I removed the comments from this code snippet to keep it short. Now, let's see how this actually works.

The Breakdown

using namespace v8;

#define MAX_SCRIPT_LEN 512

The first line simple sets a namespace so we do not have to prepend v8 to all of the calls we are making to it. The second line is setting a constant we will use later.

printf("Javascript REPL (Ctrl+C to quit)\n");

Isolate* isolate = Isolate::GetCurrent();
HandleScope handle_scope(isolate);
Handle<Context> context = Context::New(isolate);
Context::Scope context_scope(context);

Within the main function, the first few lines setups the v8 context for use. First, we get the default Isolate. V8 creates a default isolate when it is first initialized. An isolate is an isolated instance of the V8 engine. Then we create a new HandleScope. This is a class that governs all of the Handles created. Next we create a Handle to a new Context. A Handle is a reference to an object that is tracked and managed by the V8 garbage collector. We must use Handles which are known to the garbage collector to point to an object rather than pointing to it directly. A Context is a Javascript sandbox execution environment where we will execute all of our code. Finally, we must enter a context scope to execute the code within.

while (1) {
    char *scr = (char*)malloc(MAX_SCRIPT_LEN);

    printf("> ");
    fgets(scr, MAX_SCRIPT_LEN, stdin);

Now, we can enter our REPL loop. This is just a basic while loop, but it is a never ending loop. First we allocate a new char array. Then we print a prompt to the screen, and do an fgets call to read input from the user into the scr array.

Handle<String> source = String::NewFromUtf8(isolate, scr);
Handle<Script> script = Script::Compile(source);
Handle<Value> result = script->Run();
String::Utf8Value utf8(result);

printf("%s\n", *utf8);

This is more code straight from Google. The difference here is that we are replacing the hard-coded "Hello, World!" string with input from the user. This takes the src input and turns it into a javascript Script object. Then we compile it calling the Compile method of the Script object. We call the Run() method and convert the result into a utf8 string and prints it out.

free(scr);

The last thing we need to do is free up the memory we allocated to our char array.

That should be it. We are now ready to compile this thing and run it. But first, we need to point to the required dependencies for the linker.

Compiling

Right-click your project name in Visual Studio and go to Properties. This popups a dialog. In this dialog, expand the Linker group and click on Input. On the right, you will see a list of values. Click on the Additional Dependencies value. Click the down arrow and select <Edit...>.

This brings up another dialog with a blank text box at the top. You can enter the following lines:

{V8_PATH}\build\Release\lib\v8_base.ia32.lib
{V8_PATH}\build\Release\lib\icuuc.lib
{V8_PATH}\build\Release\lib\v8_snapshot.lib
{V8_PATH}\build\Release\lib\icui18n.lib
ws2_32.lib
winmm.lib

Replace {V8_PATH} with the location to your V8 folder.

You should now be able to compile the app and run it in your command line.