Extending Ejscript

Ejscript can be extended to create new types, functions, objects and variables.

Ejscript may be extended ways:

Scripting is certainly quicker, easier and more secure than creating native C modules. However, Native C coding may run faster and gives you access to the full Ejscript Virtual Machine.

Script Library Modules

Script code can be compiled and the byte code saved for use by other users as a module file. When script code is compiled, the Ejscript ejsc compiler saves the output in a module file with a .mod extension. This is often referred to as a mod file.

ejsc --out Library.mod file1.es file2.es

This example creates a pre-compiled mod file with the byte code for file1 and file2. It can be reused by other script programs without having to parse or recompile the script files. The ejs shell and the ejsc compiler accept mod files on the command line. For example:

ejs Library.mod program.es

This loads the Library module and then runs the program.es script. It would be nice not to have to explicitly specify the required modules on the command line. The module directive solves this problem. In-fact, this is what the Ejscript core libraries do to transparently load the various ejs.* modules that comprise the Ejscript core language library.

Module Directives

The module directive is an Ejscript statement that groups related declarations into a single logical module. Module directives are similar to ActionScript or Java packages.
/* In the source file anyname.es */
module mytools {
    /* Put any declarations here */
    function myFunction() {
    }
}

This example creates a function called myFunction which is included in the mytools module. When compiled by ejsc, this will create a module file with the same name as the module directive name.

If we put the above script code into a source file called anyname.es, and then compile with ejsc, we will get a module file called mytools.mod.

ejsc anyname.es
ls -l mytools*
-rw-r--r--  1 john  wheel  78 Feb 19 19:38 mytools.mod

If the compiler is invoked without the --out switch, it will automatically create module files corresponding to each module directive. Note that module blocks may span several files and that the ejsc compiler will aggregate the code from each source file into the correct module file.

When compiled, the module can be used in another program and be brought into scope via the use module directive.

/* In the source file program.es */
require testlib
myFunction()

To run the program with the ejs shell you now do not need to specify the module name. The compiler and loader will automatically resolve and load the mytools.mod module file in response to the use module directive.

ejs program.es

Installing Modules

Once your module is created, you can install it by simply copying it to the Ejscript module repository. On Unix systems, the repository is typically at /usr/lib/ejscript/modules. You should observe the Ejscript Module Repository guidelines when you install your module.

By using module directives, you can simply create a modular extension components and libraries for Ejscript that can be easily used by others. For more information, read the Modules and Namespaces and Classes documents.

Declarations

You can define global functions and variables by placing such declarations outside any module blocks. Then by using the ejsc compiler --out switch, you can explicitly name the output module file. However, module files that contain only global code with no module directives will need to be explicitly loaded and cannot be installed in the module repository. Without a module directive, the loader cannot locate the module. Users of such a module will need to explicitly load the module. A solution is to create a dummy module directive purely to give the module a name.

Native Modules

Native modules are Ejscript modules that have their implementation provided by native C code rather than by compiled byte code. Native modules have full access to the Ejscript Virtual Machine. If you need maximum control, then creating native modules is probably the way to go.

Steps to Create a Native Module

  1. Create a module interface
  2. Generate the module C header
  3. Create the native C code for the module
  4. Compile into a shared or static library

Module Interface

Defining and specifying traits of all module declarations in C can be tedious. You need to specify the names, types and qualifiers for all types, functions, function parameters, return types and variable. However, there is an easier way.

By creating a scripted module interface file and compiling this into a mod file, you are relieved of completing this task in C. The module interface is compiled like any other script to generate a mod file. When a program requests to load the module, the loader will first load the interface mod file and then load the module shared library.

A module interface file looks like a normal script file except that declarations are decorated by native qualifier, functions do not have code bodies or braces and all variables, functions and parameters are typically fully typed using type annotations.

use strict
module acme {
    native var launchAttempts: Number
    native function createRocket(kind: String): Rocket
    native class Rocket {
        private var kind: String
        private var timeToOrbit: Number
        native function Rocket()
        native function blastOff(kind: String): Void
    }
}

There are several things to explain in this example. We put the compiler into strict mode to warn if we omit the type of any variable, function parameter and or return value.

Blended Modules

The module interface can blend native code and script code. i.e. It can contain actual script code without a native qualifier. In this way, you can choose on a function by function basis whether a function is best implemented as script code or as native C code.

Generate the Module C header

After compiling the module interface into a mod file, we can generate a C header that describes the locations of the declarations in the VM. Where possible, the Ejscript compiler early-binds declarations into slot offsets which are property indices into the relevant objects. Early binding enhances the speed and quality of the generated byte code. For example, variables can often be accessed by numerical slot offset rather than doing a costly run-time lookup by name through the scope chain.

The ejsmod module manager tool is used to generate the C header.

ejsmod --cslots acme.mod

This creates a acme.slots.h C language header file that should be included by that module native C code. You can use the slot definitions with the Ejscript APIs like ejsGetProperty which will take the symbolic slot offset as an argument.

The header contains definitions like:

/**
     Class property slots for the "Rocket" class
  */
#define ES_acme_Rocket__origin             5
#define ES_acme_Rocket_Rocket              5
#define ES_acme_Rocket_blastOff            6
#define ES_acme_Rocket_NUM_CLASS_PROP      7
/**
   Instance slots for "Rocket" type
  */
#define ES_acme_Rocket_kind                0
#define ES_acme_Rocket_timeToOrbit         1
#define ES_acme_Rocket_NUM_INSTANCE_PROP   2
/**
      Local slots for methods in type Rocket
 */
#define ES_acme_Rocket_blastOff_kind       0

The defines follow the form: ES_module_class_function_declaration. Where class and function are omitted for declarations outside classes or functions.

Creating the Module Native C Code

When Ejscript loads the module interface file, it creates and configures objects for all the declarations in the interface. All that remains to be done is create the native code implementation for the C functions and bind them to the defined JavaScript functions.

But first we need to create a loadable module if we are building shared.

Create the Loadable Module

The loadable module is a shared library containing the native class implementation and a library entry point function. The function must be named MODULE_ModuleInit, where MODULE is the name of your module with all dots changed to underscores.

#include "ejs/ejs.h"
#include "acme.slots.h"
int acmeModuleInit(Ejs *ejs, MprModule *module)
{
    EjsType     *type;
    EjsName     qname;
    /* Get the Rocket class object */
    type = ejsGetTypeByName(ejs, ejsName(ejs, "acme", "Shape"));
    /* Bind the C functions to the JavaScript functions */
    ejsBindConstructor(ejs, type, constructor);
    ejsBindMethod(ejs, type->prototype, ES_acme_Rocket_blastOff, blastOff);
    return module;
}

This entry point will be invoked after the Ejscript loader loads the mod file and the shared library.

The module object created by mprCreateModule is used by the MPR runtime to manage the module. The "rocket" name does not really matter. Just provide reasonably descriptive name for the module library.

The Rocket type has already been created via the mod file and so we can just get it as a property of the global object. All types are stored as global properties.

Lastly, we can call ejsBindMethod to bind the C function implementations to the JavaScript functions. In this case, we bind the constructor function and the blastOff function.

Create the Native C functions

static EjsObj *constructor(Ejs *ejs, EjsObj *rocket, int argc, EjsObj **argv)
{
    ejsSetProperty(ejs, rocket, ES_acme_Rocket_kind, argv[0]);
    return rocket;
}
static EjsObj *blastOff(Ejs *ejs, EjsObj *rocket, int argc, EjsObj **argv)
{
    ejsSetProperty(ejs, rocket, ES_acme_Rocket_timeToOrbit, 
        ejsCreateNumber(ejs, 600));
    return 0;
}

Things to Remember

By creating a module interface definition, the VM is able to check and cast all function arguments to the correct type. This greatly simplifies writing native functions — you don't need to check type number or types of the arguments.

For each function, think if it should be created in script code or in native code. You can often code functions that are not executed as frequently in script code without impacting performance. It is a good strategy to prototype the module in script first and then convert the prototype to native code on a function-by-function basis.

Native API

Ejscript has an extensive native API for interacting with the Ejscript virtual machine. Because types are themselves objects, the API offers a consistent way to access properties from objects or classes. Some of the more important API functions are listed below. See the full Ejscript API and the MPR API reference for full details.

API Name API Description
ejsAlloc Allocate a new object instance
ejsBindMethod Bind a native C function to a JavaScript property
ejsCast Cast a variable to another type
ejsClone Create a clone copy of a variable
ejsCreateArray Create a new array
ejsCreateBareString Create an empty string of the required length
ejsCreateBoolean Create a Boolean value
ejsCreateByteArray Create a ByteArray object
ejsCreateDate Create a Date object
ejsCreateFile Create a File object
ejsCreateFunction Create a Function object
ejsCreateIterator Create an Iterator object
ejsCreateNumber Create a Number object
ejsCreateObject Create an object and reserve space for properties
ejsCreateRegExp Create a RegExp (regular expression) object
ejsCreateSimpleObject Create a simple, empty object
ejsDefineGlobalFunction Define a public global function and bind to a C function
ejsDeleteProperty Delete a property from an object
ejsDeserialize Deserialize a string and create an equivalent object
ejsExit Exit the application
ejsGetBoolean Get a native boolean value from a Boolean object
ejsGetInt Get a native integer value from a Number object
ejsGetNumber Get a native number from a Number object
ejsGetProperty Get a property from an object
ejsGetVarType Get the underlying type of a variable
ejsGetDouble Get a native double value from a Number object
ejsGrowObject Grow the space allocated for properties in an object
ejsIsA Determine if an object is of a given type
ejsLookupProperty Lookup a property by name in an object
ejsParseVar Parse a string and create an equivalent variable
ejsRun Run a script
ejsRunFunction Run a function
ejsRunFunctionBySlot Run a function at a given property slot
ejsSerialize Serialize an object into an equivalent string representation
ejsSetProperty Set a property's value
ejsSetPropertyByName Set a property's value using the property name
ejsSetPropertyName Update the property's name
ejsThrowError Throw an error exception
ejsToString Convert (cast) the argument to a string

Samples

A library of samples is provided with the Ejscript source code. See the samples directory for many examples of embedding and extending Ejscript.

For information about embedding Ejscript into applications, see the Embedding Ejscript

document.

© Embedthis Software. All rights reserved.