Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
67 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

External Functions

One of the most important things for a scripting system to be able to do is bind with code that you have written to provide non-trivial and/or high-performance functionality for an application. Games and other multi-media applications normally require such functionality. Creating hard-coded instruction types to deal with one particular external system or another is not very flexible, and will prevent you from easily reusing the same scripting system for other purposes. A consistent method of calling external code is to be preferred, and function pointers are a good way to achieve this.

Callbacks

The relationship between the virtual machine and some external system will be described through callbacks that make use of pointers to member functions of the external system. These callbacks, as well as a reference to an external system, will be stored in the virtual machine, to be called by scripts that use a set of calling instructions, which we will create in the next section.

To illustrate a simple, yet flexible, callback system, the member functions called will take an argument vector as a parameter and possibly return some value. This is much like the common "int main(int argc, char* argv[])", except the argument count will be embedded in the vector's size(). A generic solution to defining such callbacks follows:

template <class T, typename ReturnType, typename ArgType>
class Callback
{
public:
    typedef std::vector<ArgType>    ArgVector;
    typedef ReturnType (T::*Func)(const ArgVector&);
    explicit Callback(Func f) : _f(f)   {}
    ReturnType operator()(T& t, const ArgVector& argv) const   { return (t.*_f)(argv); }
private:
    Func    _f;
};

template <class T, typename ReturnType, typename ArgType>
Callback<T, ReturnType, ArgType> make_callback(ReturnType (T::*func)(const std::vector<ArgType>&))
{
    return Callback<T, ReturnType, ArgType>(func);
}

These templates form a simple interface for creating callbacks from member function pointers, and then calling them through a given object reference. The return and argument types can be chosen at our discretion. Because the Callback constructor is explicit to avoid implicitly creating callbacks from function pointers, the "make_callback" template is useful for constructing such callbacks without having to redundantly define the template parameters of the particular Callback being created. The reasoning is that the compiler should already know what kind of callback to create given a particular function pointer.

Using the Callbacks

Now that we have these callbacks, it's time to put them to use. The first thing to do is establish this relationship between the virtual machine and an external system. To do this we will template the virtual machine according to a SystemType. Some clerical changes have to be made to foster this, including moving the virtual machine code from the cpp into the header. We can then define a "Command" as a Callback in terms of the SystemType. For this example, we will be using a callback type that takes integers as arguments and returns an integer, so we can also define the argument vector to save us some typing:

template <class SystemType>
class VirtualMachine
{
private:
    // example system command taking int arguments, and returning an int
    typedef Callback<SystemType, int, int>  Command;
    typedef std::vector<int>                ArgVector;
. . .

When instantiating a virtual machine, we will pass it a system reference to bind itself to, as well as the system's command interface in the form of a list of function pointers. These pointers will then be consolidated into a list of commands. The straightforward procedure for creating system callbacks uses the "make_callback()" utility defined earlier:

VirtualMachine(SystemType& system, const Command::Func funcList[], size_t numFuncs)
    : _system(system)    { CreateSystemCallbacks(funcList, numFuncs); }

void CreateSystemCallbacks(const Command::Func funcList[], size_t numFuncs)
{
    for (size_t i = 0; i != numFuncs; ++i)
        _commandList.push_back(make_callback(funcList[i]));
}

The last thing to implement purely with the virtual machine itself is a way to call one of its stored commands by ID, and passing it an argument list. Needless to say, when we begin using a higher-level language, system call IDs will be resolved during compilation. This functionality makes straightforward use of the Callback:

template <class SystemType>
int VirtualMachine<SystemType>::CallCommand(size_t id, const ArgVector& argv)
{
    assert(id < _commandList.size());
    return _commandList[id](_system, argv);
}

A few changes now have to be made to the virtual machine's execution class. First, in order to be able to make a system call through the virtual machine, it needs to be passed a reference to the machine when it is told to execute, which is simple enough:

void Execute(VirtualMachine& vm, ScriptRef scriptPtr);

Using new instructions that will be provided shortly, a script will be able to pass variable or constant arguments to an argument vector stored in the execution. Using a provided reference to a virtual machine, and this argument vector member, a system call by ID is simple to make from the execution:

int CallSystemCommand(VirtualMachine& vm, size_t id)  { return vm.CallCommand(id, _argv); }

Finally, to be implemented are three new instructions for working with system commands:

// system commands
case op_pusharg_const:
    _argv.push_back(_instr->Data()[0]);
    ++_instr;
    break;
case op_pusharg_var:
    _argv.push_back(_stack[_instr->Data()[0]]);
    ++_instr;
    break;
case op_call_command:
    _register = CallSystemCommand(vm, _instr->Data()[0]);
    _argv.clear();
    ++_instr;
    break;

The first two instructions resemble those used for pushing variables or constants onto the stack. The main difference is the destination of the push, which is now the argument vector. The next instruction simply calls the system command through the execution's interface, and stores the result in the general-purpose register. The argument vector is then cleared. Use of these instructions is quite simple.

This is an example of calling a command with an ID of 0, taking two parameters. The first parameter is the value 5, and the next is the value of the variable at index 3. The result is then placed on the stack (which is optional):

pusharg_const 5
pusharg_var 3
call_command 0
load




Page 4

Contents
  Introduction
  Page 2
  Page 3
  Page 4

  Source code
  Printable version
  Discuss this article

The Series
  An Introduction
  Data Manipulation
  Dynamic Loading
  The Stack and Program Flow
  Functionality