Calling script functions with parameters

2004/12/01, Andreas Jönsson

Generally speaking the script functions use the same calling convention as global C functions, i.e. the parameters are pushed on the stack in reverse order and the return value is returned in a register, unless it is an object in which case it is returned in memory allocated by the calling function.

Preparing context and executing the function

Normally a script function is executed in a few steps:

  1. Prepare the context
  2. Set the function arguments
  3. Execute the function
  4. Retrieve the return value

This is assuming a script context is already available. The code for this might look something like this:

// Prepare() must be called to allow the context to prepare the stack
context->Prepare(engine->GetFunctionIDByDecl(module_name, function_declaration);

// SetArguments() is used to push the arguments on the stack
context->SetArguments(...);

int r = context->Execute();
if( r == asEXECUTION_FINISHED )
{
  // The return value is only valid if the execution finished successfully
  context->GetReturnValue(...);
}

If your application uses ExecuteStep() or registers a function that allow the script to suspend its execution, then the execution function may return before finishing with the return code asEXECUTION_SUSPENDED. In that case you can later resume the execution by simply calling the execution function again.

Note that the return value retrieved with GetReturnValue() is only valid if the script function returned successfully, i.e. if Execute() returned asEXECUTION_FINISHED.

Passing and returning primitives

When calling script functions that take arguments, the values of these arguments must be pushed on the context stack after the call to Prepare() and before Execute(). The arguments are pushed on the stack using the method SetArguments():

int SetArguments(int stackPos, asDWORD *data, int count);

stackPos is the position of the argument on the stack in dwords. It is determined by summing the size of all previous parameters. data is a pointer to the actual data the will be pushed on the stack. count is the size in dwords of the data that should be pushed on the stack.

  // The context has been prepared for a script 
  // function with the following signature:
  // int function(int, double, void*)

  // Put the arguments on the context stack, starting with the first one
  int ival = 0;
  ctx->SetArguments(0, &ival, 1);
  double dval = 0.0;
  ctx->SetArguments(1, &dval, 2);
  void *pval = 0;
  ctx->SetArguments(3, &pval, 1);

It would also be possible to build a buffer with all the arguments and then copy the entire buffer in one call to SetArguments().

Once the script function has been executed the return value is retrieved in a similar way using the method GetReturnValue():

int GetReturnValue(asDWORD *data, int count);

data is a pointer to where the return value is to be copied. count is the size of the value in dwords.

Note that you must make sure the returned value is in fact valid, for example if the script function was interrupted by a script exception the value would not be valid. You do this by verifying the return code from Execute() or GetState(), where the return code should be asEXECUTION_FINISHED.

Passing and returning objects by value

AngelScript is recognizably lacking when it comes to passing objects to script functions, especially if the objects require special care because of allocated resources. Future versions will remedy this, in the meantime, here is an explanation of how it can be done.

Passing a simple object like struct Vector3 { float x; float y; float z; } is as simple as passing a primitive, just call SetArguments() and pass the pointer to the object and the size of the object in dwords.

Passing a more complex object that allocate and free resources is a lot more complicated. If you try to simply copy such an object to the context stack, then you will be creating a duplicate. When this duplicate is later destroyed it might destroy resources still held by the original object (or vice versa). What you need is a way to move the object into the context stack.

// The complex object we wish to pass to the script function
CObject obj;

// Allocate some generic memory
char *mem = new char[sizeof(CObject)];

// Copy construct an object into the generic memory. This
// should take care of the resources allocated by obj. 
// The placement new operator is declared in new.h.
new(mem) CObject(obj);

// Copy the memory into the context stack.
Now the context is responsible for deleting the object.
ctx->SetArguments(pos, mem, ((sizeof(CObject)+3)&~3)/4);

// Free the memory without destroying the object
delete[] mem;

This piece of code is using some C++ trickery to make a valid duplicate of the object. Valid in that it would also duplicate the resources, or correctly share them depending on how the resources are handled. This of course requires that the class was in fact correctly implemented to support the copy constructor. One the duplicate has been copied into the context stack the C++ code must not delete the object, which is why we never call the object's destructor.

Getting an object returned by a script function is done in a similar way. It's complicated further because the C++ application must allocate the memory for the object before calling the script function. The pointer to this memory should be passed to the script function as the first argument, i.e. in position 0, and all other arguments have their position increased with 1.

// The object where we want to store the return value
CObject obj;

// Allocate memory for the returned object.
// Make sure no object is constructed in the memory.
char *mem = new char[sizeof(CObject)];

// Pass the pointer as the first argument
ctx->SetArguments(0, &mem, 1);

// Execute the function
int r = ctx->Execute();
if( r == asEXECUTION_FINISHED )
{
  // Now the memory has been initialized with the object.
  // If we called GetReturnValue() we would receive the pointer to the memory
  
  // Copy the object to our true object
  obj = *(CObject*)mem;
  
  // Destroy the object before freeing the memory
  ((CObject*)mem)->~CObject();
}

// Free the memory
delete[] mem;

In this piece of code it is important to note that the object is destroyed by explicitly calling the objects destructor, before the memory is freed. If the object was a simple struct, then it would not be necessary to call its destructor, but it would still be necessary to allocate the memory and pass the pointer as shown in the code.