Links

Categories

Tags


« | Main | »

Native Types in JewelScript

By Jewe | October 11, 2007

This article explains the term Native Types and gives a brief and general overview on how they are implemented.

Native types are classes or collections of global functions, written in C or C++, and bound to the JewelScript language, making it possible to use these functions and classes from within the script language.

When you use the JILRuntime library as-is, meaning without adding any additional native types, there is only a handful types built-in into the runtime, like for example the string, array and table classes. In principle, you can view the runtime as being only the kernel of the language, with only a minimal set of built-in functionality.

This approach keeps the runtime lean, and makes sure there is no excess functionality in the library that you don’t need, is unsuitable or unsafe for your type of application, and hence would never be used or must not be used by your target audience.

Of course, you can write classes in script to extend the functionality of the language. However, there are certain scenarios that make it necessary to implement a function in C or C++ and bind it to the runtime.

The basic structure of a native type

Native types in JewelScript follow a plug-in architecture design scheme. This means there is a central generic C procedure that handles all the interfacing between the script world and the C or C++ world. This generic procedure, a callback function, is termed the type proc. All runtime information queries, and all function and method calls to your native type will be handled through this function. So in order to write our own native type for JewelScript, we need to write a type proc.

The declaration of a type proc looks like this:

JILLong MyNativeTypeProc (NTLInstance* pInst, JILLong message,
    JILLong param, JILUnknown* pDataIn, JILUnknown** ppDataOut);

In order to bind a native type to the runtime, you register it to the runtime by calling the JILRegisterNativeType() API function. This simple process associates your type proc with a user defined type name, which is used as the class name or namespace in JewelScript.

// registering to the runtime
JILError err = JILRegisterNativeType( m_pRuntime, MyNativeTypeProc );

This little one-liner is enough to make your entire class and all of its functions and methods known to the runtime. After this, your scripts can use the type by using the import statement and the type name you chose for your native type.

How does this work? How does the language “know” the type name associated to your type proc, and how does it know what functions and methods your type proc can handle?

Type information queries

At certain points, for example while registering your type proc to the runtime, the runtime will send various messages to the type proc in order to gather information about your native type. One of them, NTL_GetClassName, queries the type name of the native type for example.

In general, the runtime will pass additional data with messages to your type proc through the pDataIn pointer, and expects the type proc to return pointers to return values (where necessary) by writing them to the ppDataOut pointer. In the case of a NTL_GetClassName message, your type proc is supposed to respond by writing the address of a C-string defining the type name to ppDataOut.

case NTL_GetClassName:
    (*(const JILChar*) ppDataOut) = "MyClass";
    break;

Now you know the mechanism how the type name is associated to a native type. Declaring any functions and methods your native type implements works in a very similar way.

When the JewelScript compiler imports your native type, because it compiled an import statement with your type name, it will send an NTL_GetDeclString message to your type proc. Your type proc can respond to this message by returning a C-string declaring your whole native class in ppDataOut. In principle, this C-string will look exactly like a class declaration statement in script, and in fact the compiler will do nothing but format the string into a full class declaration statement that is then compiled.

case NTL_GetDeclString:
    (*(const JILChar*) ppDataOut) = kMyClassDeclaration;
    break;

Where the constant kMyClassDeclaration could look like this:

const JILChar* kMyClassDeclaration =
    "method           MyClass();"
    "method           SomeMethod(string str);"
    "function string  SomeGlobalFunc();"
    "const int        kSomeConstant = 27;";

This string constant declares two methods, one of them being your class constructor, a global function and a global constant for your native type “MyClass”. Note that the order used when specifying functions and methods determines the value of their function index.

Function indexes are used by the runtime when calling one of your native functions or methods. For example, if the runtime wanted to call the “SomeGlobalFunc” function of our native type, it would call the type proc with message = NTL_CallStatic, and param = 2. Responding to this message could look like this:

case NTL_CallStatic:
{
    JILState* pState = NTLInstanceGetVM(pInst);
    switch( param )
    {
        case 2:
           NTLReturnString( pState, "Hello World!" );
           break;
    }
    break;
}

This rudimentary message handler will check the given function index, and if it is 2, it will return the string “Hello World!”. With this, our native function “SomeGlobalFunc” has been implemented. Of course, native types are usually more complex and contain more than one function, so using nested switches directly in the type proc is not a good idea. A good implementation would dispatch messages into sub-routines, rather than handling them directly. We’re just keeping it simple here for demonstration purposes.

Calls to methods are handled quite similar. Actually, the only difference is that the runtime will call your type proc with message = NTL_CallMethod, and the pointer to the MyClass instance (the this-pointer if you will) in pDataIn.

The plug-in architecture

If you have some experience with other scripting libraries you will probably notice how different from others binding a native type to the JILRuntime works. If you just want to bind a single native function to the script runtime, then writing a full type proc on top of the native function you want to write seems like a lot of overhead. In fact, there’s no doubt it is a lot of overhead.

The design of the native type interface in JewelScript has been chosen to make adding full classes or sets of global functions in namespaces as simple as possible, because in the long run, your simple application wanting to only bind a single native function will become larger – and adding more native functions or even full classes will eventually become necessary.

Apart from that, the type proc as a single entry point function to handle the whole native type interface in a generic way, is ideal to put native types into shared libraries. Using this design, the runtime is already set for the case where developers would like to put all their native types into dynamic link libraries and have the runtime dynamically load them from a certain “plug-ins” folder at start-up.

Adding a new native type to the runtime would then be a matter of copying a DLL / bundle / shared library file into a folder and it is ready for usage in scripts.

When you take a look at the type procs of the built-in classes, you will see that they are all in large parts identical. So writing sort of a wrapper class or template in C++, or writing a code generator should not be a big problem.

In addition, starting with JewelScript 1.0, the library has a built-in C++ code generator, which makes it very easy to automatically create the binding code for any C++ class or function.

Topics: docs | Comments Off on Native Types in JewelScript

Comments are closed.