Links

Categories

Tags


« | Main | »

Embedding JewelScript – Tutorial I: Getting started

By Jewe | October 2, 2007

This is the first of a number of tutorials that show how to embed the JewelScript library into your application in order to add scripting features to it. I will show some example code and give a detailed discussion for each code fragment.

December 2014: This tutorial is rather outdated. The JewelScript API has been subject to changes since this article was written. For the most part, this information is still accurate. However, everything said in Section 6, “Calling our script function” is obsolete. The way script functions are called from the native side has been greatly simplified since version 0.9. This post explains the changes in detail.

This very basic example will show you how to create an instance of the runtime, and have it compile and run some script code.

1. Initializing the runtime

Even though JewelScript is written in ANSI C, it is still written in an object-oriented manner. You can have an unlimited number of instances of the runtime in your application if you want. In order to create an instance, you have to call the JILInitialize() function, declared in “jilapi.h”. Additionally, we will need the functions from “jilcompilerapi.h” for this example.

Calling JILInitialize() will allocate and initialize the main runtime object, called JILState, and return a pointer to it. You don’t really need to care about what members this object has. Instead, you just pass this pointer to all other API functions you want to call. Typically, you want to store this pointer in a member variable of your application class, unless you want to create, execute and destroy the runtime within the same function, which is rather unusual (and inefficient).

Note that since v0.9, calling JILInitialize() will also automatically create an instance of the JewelScript compiler – in earlier versions you had to do this “manually”.

m_pRuntime = JILInitialize( 1024, NULL );
if( m_pRuntime == NULL )
    return E_FAIL;

When you create your script runtime, you have to make the first important decision: specify a stack-size. In the above example the stack is set rather small, to 1024 entries. The stack-size basically specifies how much data you can pass to a function when it is called, and how much local variables you can use in the function’s body.

If your script uses nested function calls (a function calling a function, calling a function, and so on) then of course all the arguments and local variables of all functions need to fit onto the stack. Note that the runtime will save additional data onto the stack when a function is called, for example virtual machine registers that the function is going to use will be saved there.

To make sure the virtual machine does not run out of available memory on the stack, which would cause a bad crash (unless you run the debug build of the library or have enabled extended runtime checks – in which case a virtual machine exception will occur), specify a sufficient stack-size when creating the runtime.

When in doubt if your stack-size is sufficient, you can test-run your scripts using the debug build of the runtime, which will do run-time checks and throw a virtual machine exception when running out of stack space. Even if you don’t install a custom exception handler (we will discuss this in a later tutorial), you will be notified if this exception occurs, because the default behavior of the virtual machine is to abort execution of the script and returning an error code.

The second argument to the JILInitialize() function can be used to pass an additional character string with compiler options to the runtime. When the function initializes the compiler, it will pass these options on to it. That way, we could enable optimizations and such, but for now it is sufficient to leave that argument NULL.

2. Installing a logging callback

Before we start compiling and running code, we can call additional, optional functions to set certain options for the runtime. One of the more important options is installing a logging callback function. The runtime will use the logging callback to output status messages, compiler warnings and errors, and when critical runtime errors have occured. If no callback is set, this information will not be printed anywhere, so it is quite important to have such a callback installed.

JILSetLogCallback(m_pRuntime, MyLogCallback);

A logging callback can look as simple as this:

static void MyLogCallback( JILState* pState, const char* pMessage )
{
    fprintf(stdout, pMessage);
}

As we can see, this simple function does nothing but print the given message to the console. In graphical applications (especially under Windows) this might not work very well. In that case we could open a file when creating the runtime and attach the file-handle as user-data to the runtime. When our logging callback is called, we can then obtain the file-handle from the runtime and print the given string into the file. Of course, another possibility is to open a message box and prompt the given message to the user.

3. Compiling script code

There are actually a number of options how to compile script code, some are intended to dynamically create and run functions, while others just statically compile one or more scripts. Depending on the nature of your application, one method might be better for what you are trying to achieve than the other. This simple example assumes you just want to load a script file when your application starts and compile it. Then run the script function at any given time later on.

If the script is a file on the local file system, you can simply use the JCLLoadAndCompile() function to do the job.

JILError err = JCLLoadAndCompile(m_pRuntime, "main.jc");
if( err )
    return err;

I’m using the extension “.jc” for JewelScript files. However that doesn’t mean you have to. For the file name specified to JCLLoadAndCompile() you can simply use any extension, the file will be loaded regardless. If your script file imports more files from the file system using the import statement, the compiler will assume the extension “.jc”. However, there is a compiler option you can use to change this.

Not all applications will want to allow scripts to import more script files from the local file system. There is a preprocessor switch in “jilplatform.h” that you can use to disable importing script files. Additionally, there is a compiler option that lets you disable this. Of course, this will not disable the ability to import native types you register to the runtime.

Sometimes, you don’t want to load a script file from disk, but have it elsewhere. For example in your resources, in a C-string constant, or whatever. In this case you can use the JCLCompile() function to compile a piece of script code directly from a given C-string. Note that the given string must contain syntactically and semantically correct script code. Trying to compile the first half of a function and then the second half with another call will not work.

static const char* kMyScriptConstant =
    "function int add(int a, int b) { return a + b; }";
JILError err = JCLCompile( m_pRuntime, "MyScriptConstant",
    kMyScriptConstant );
if( err )
    return err;

This will compile the small function add() defined by the given C-string constant. The argument “MyScriptConstant” has no specific meaning for the runtime – it will only be used as a reference when compiling the script causes an error or warning. If you compile many script fragments, specifying a sensible name will help you find the fragment that caused the error / warning.

All errors and warnings that the call to JCLLoadAndCompile() or JCLCompile() might produce will be written to the logging callback function. In the case of one or more errors, the last error code will be also returned by the respective function. Additionally, you can call JCLGetErrorText() subsequently to retrieve any errors or warnings produced by the script. This is useful if you need further processing of any warning and error messages before presenting them to the user.

If compiling script code failed, you should not attempt to execute the respective function the script should have generated. However, it is relatively safe to call other functions that previous compiles might have generated.

4. Linking

You can call the compile functions as often as you like to compile script code, as long as there are no multiply defined symbols or other errors. Once you are done compiling all your code, you have to call JCLLink() to create a single executable out of all the code fragments.

JILError err = JCLLink( m_pRuntime );
if( err )
    return err;

Trying to execute any code before linking will result in an error code because the virtual machine will have no byte-code to execute at all. The link process will “fill” the virtual machine’s code segment with the accumulation of all compiled classes and functions.

The link process is also important if you will be using JewelScript’s byte-code optimizing feature. During this process, the optimizer will run through the code generated by the compiler and try to optimize it.

You may call one of the compile functions again to add more functions or classes to the program after linking – even after running the program – however, if you do you also need to link again for the new functions and classes to be available to the virtual machine.

5. Executing the “Init Code”

Before we can finally execute our script function, one last step is necessary. We need to run through the “Init Code” that the compiler has generated, in order to initialize all global variables and constants. Even if you know exactly that your script code does not use any global variables or constants, it is good practice to do this. You or someone else might change your scripts in the future and use globals, which would be undefined if you don’t call JILRunInitCode().

JILError err = JILRunInitCode( m_pRuntime );
if( err )
    return err;

After this we are finally set-up and can use the runtime to execute our script as often as we like. It is probably a good idea to put all of the above code into a single method that our application class can call in order to initialize our runtime. Here is what such a method could look like:

int CMyApp::Initialize()
{
    m_pRuntime = JILInitialize( 1024, "" );
    if( m_pRuntime == NULL )
        return E_FAIL;
    JILSetLogCallback( m_pRuntime, MyLogCallback );
    JILError err = JCLCompile( m_pRuntime, "MyScriptConstant",
        kMyScriptConstant );
    if( err )
        return err;
    err = JCLLink( m_pRuntime );
    if( err )
        return err;
    err = JILRunInitCode( m_pRuntime );
    if( err )
        return err;
    return E_SUCCESS;
}

6. Calling our script function

Now that everything has been set-up, we want to call our little script function add(). In order to do that, we must first know the function index of that function. The function index is a unique integer number assigned to every function the compiler creates. We can use JILGetFunction() to get the function index from a global function.

JILLong fnAdd = JILGetFunction( m_pRuntime, NULL, "add" );
if( fnAdd == 0 )
    return E_FAIL;

Note that this function will not take arguments or result type into account when searching for the specified script function – it will return the index of the first function with a matching name.

Now that we know the function index, lets call the script function with 100 for argument ‘a’ and 50 for argument ‘b’:

JILError err = JILBeginCall( m_pRuntime, NULL, 2,
    kArgInt, 100,
    kArgInt, 50);
if( err )
    return err;
err = JILCallFunction( m_pRuntime, fnAdd );
if( err )
    return err;
int iResult = JILGetResultInt( m_pRuntime );
JILEndCall( m_pRuntime );

Now this looks kinda “complicated”, so let me explain what all those calls are doing. The first call, JILBeginCall() mainly has the purpose to push the function arguments onto the virtual machine’s stack. Our add() function expects these values on the stack – the variable names ‘a’ and ‘b’ are actually references to values on the stack.

The next call, JILCallFunction() is pretty self-explanatory, our script function is being executed. If an error or unhandled exception should occur during execution, this call will return an error code.

After our script function has returned, we want to get access to the result it returned, this is done by calling JILGetResultInt(). There are similar functions called JILGetResultFloat() or JILGetResultString() that you can use if your function returns values of a different type. Obviously, if your function does not return a result, you can (and should) omit this step – getting a result from a function that doesn’t return one will give you unpredictable results.

Finally, we call JILEndCall(). The main purpose of this function is to “clean up” the stack for us. Since we have altered the stack by pushing values onto it in JILBeginCall(), we have to remove them now that we are done. JILEndCall() will automatically take care of this. It will also free any result that the function has returned, so JILGetResultInt() and similar functions should always be called before calling this function.

Actually this example code is a bit inefficient. We don’t have to ask the runtime for the function index of our add() function everytime we want to call it. Searching for a function can become quite slow if we have compiled hundreds of global functions, so we should do this only ONCE. The function index associated to a function never changes anyway, so I recommend to move the JILGetFunction() call to the CMyApp::Initialize() method and store the index in a member variable, named m_FnAdd for example.

In order to call our script function easily and with arguments that are not hardcoded, we could write the following function for our application class:

int CMyApp::CallAdd(int a, int b)
{
    if( m_pRuntime == NULL || m_FnAdd == 0 )
        return 0;
    JILError err = JILBeginCall( m_pRuntime, NULL, 2,
        kArgInt, a,
        kArgInt, b);
    if( err )
        return 0;
    err = JILCallFunction( m_pRuntime, fnAdd );
    if( err )
        return 0;
    int iResult = JILGetResultInt( m_pRuntime );
    JILEndCall( m_pRuntime );
    return iResult;
}

Now we can just write CallAdd(x, y) anywhere in our program and don’t even notice that behind the scenes it is a script function that is being executed. Of course this function doesn’t really handle any errors very well, it will just return zero if anything fails, but I’ll leave it up to you to find better solutions for that problem.

Suffice to say that the whole example is a little moot, because there aren’t really many different ways you could implement “add(a, b)”. I should probably have called the script function “compute(a, b)”. Then the application could compute a value out of two given values depending on how the script is implemented. But anyway, I reckon you’ll get the idea.

7. Terminating the runtime

Now you know how to initialize the runtime and how to compile and run a script function. Last thing you need to know is how to properly terminate the runtime. Well, for this little example, nothing could be simpler:

JILError err = JILTerminate( m_pRuntime );
if( err )
    return err;

This will free all objects allocated by the runtime and the compiler. You should make sure that you don’t call any other functions anymore after having called this function. It is unsafe to use the m_pRuntime pointer for anything after having terminated the runtime.

void CMyApp::Terminate()
{
    if( m_pRuntime )
    {
        JILTerminate( m_pRuntime );
        m_pRuntime = NULL;
    }
}

Example source code

The code of this tutorial is now also available as full example source code. See this article for more information.

Topics: embedding | Comments Off on Embedding JewelScript – Tutorial I: Getting started

Comments are closed.