Howto:Extend Nasal: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
mNo edit summary
Line 206: Line 206:
  void naHash_delete(naRef hash, naRef key);
  void naHash_delete(naRef hash, naRef key);
  void naHash_keys(naRef dst, naRef hash);
  void naHash_keys(naRef dst, naRef hash);
 
  // Retrieve the specified member from the object, respecting the
  // Retrieve the specified member from the object, respecting the
  // "parents" array as for "object.field".  Returns zero for missing
  // "parents" array as for "object.field".  Returns zero for missing

Revision as of 19:53, 16 May 2009

This article is a stub. You can help the wiki by expanding it.

This article is dedicated to describing how to write custom C/C++ extension functions in order to extend the Nasal interpreter in FlightGear, for example in order to expose new FlightGear APIs to Nasal, so that scripts can access more FlightGear internals.

So, you will likely need to be interested in FlightGear core development, need to be somewhat familiar with C/C++, as well as with some basic Nasal. Also, you should have experience compiling FlightGear (see Building FlightGear for Linux instructions or Building Flightgear - Windows for Windows specific instructions).

Introductory information on the Nasal programming language itself can be found at Nasal scripting language. Information on writing simple Nasal scripts can be found at Howto: Write simple scripts in Nasal. Useful Nasal snippets can be found at Nasal Snippets. A Nasal Style Guide is available at Nasal Style Guide (Note that as of 05/2009, this is work in progress).

If you have any Nasal specific questions, you will want to check out Nasal FAQ, feel free to ask new questions or help answer or refine existing ones.

All Nasal related articles can be found in the Nasal category.

Note: FlightGear's version of the Nasal interpreter is maintained in the SimGear CVS repository, inside the source/simgear/nasal folder, the most important header file detailing the internal Nasal API is "nasal.h", you will want to check this out for the latest changes and information.

Important: As of 05/2009, this article is work in progress, and none of the examples have so far been tested/compiled. Your help in updating this article is appreciated, thanks!

Intro

In FlightGear, the simplest way to add new extension functions is to look at the existing functions in $FG_SRC/Scripting/NasalSys.cxx (src/Scripting/NasalSys.cxx).

There is a static table of function pointers referencing extension functions, along with their corresponding names in Nasal. The following is a copy of the extension function list, taken in 05/2009:

 // Table of extension functions.  Terminate with zeros.
 static struct { const char* name; naCFunction func; } funcs[] = {
   { "getprop",   f_getprop },
   { "setprop",   f_setprop },
   { "print",     f_print },
   { "_fgcommand", f_fgcommand },
   { "settimer",  f_settimer },
   { "_setlistener", f_setlistener },
   { "removelistener", f_removelistener },
   { "_cmdarg",  f_cmdarg },
   { "_interpolate",  f_interpolate },
   { "rand",  f_rand },
   { "srand",  f_srand },
   { "abort", f_abort },
   { "directory", f_directory },
   { "parsexml", f_parsexml },
   { "systime", f_systime },
   { "carttogeod", f_carttogeod },
   { "geodtocart", f_geodtocart },
   { "geodinfo", f_geodinfo },
   { "airportinfo", f_airportinfo },
   { 0, 0 }
 };

So, the basic format is "name", function_pointer - whereas "name" refers to the name used by Nasal, and function_pointer has to use the right function signature:

// The function signature for an extension function:
typedef naRef (*naCFunction)(naContext ctx, naRef me, int argc, naRef* args);


You will need to add your new extension function to this list of static functions, preferably following the existing naming convention (i.e. "f_" prefix).

If your extension functions are likely to be fairly low level, and will thus be provided with a more abstract wrapper in Nasal space, these functions should use a prepended undercore ("_"), such as the _fgcommand, _setlistener, _cmdarg and _interpolate functions.

Extension Function Signature

These extension functions look like:

 static naRef f_your_function(naContext c, naRef me, int argc, naRef* args)
 {
     // ...
 }

So, this is basically the boilerplate that you'll need to use in order to expose a new function (i.e. you can just copy & paste this into the NasalSys.cxx file).

If you don't have anything else to return, you can just return naNil(), so that the function looks like this:

 static naRef f_cool (naContext c, naRef me, int argc, naRef* args)
 {
     return naNil();
 }

In order to make this function known to the Nasal interpreter, you'll want to extend the previously mentioned list of Nasal extension functions, to read:

 // ....
   { "airportinfo", f_airportinfo },
   { "cool", f_cool }, //this is where we add our new function
   { 0, 0 }
 };

Once you have made these modifications, you can simply invoke your new function, of course it will not yet do anything useful, though.

So, in order to make it slightly more interesting, we are going to change the return statement to return something else, instead of nil:

 static naRef f_cool (naContext c, naRef me, int argc, naRef* args)
 {
     const char* cool="cool";
     naRef retval;
     naStr_fromdata(retval, cool, strlen(cool) );
     return retval;
 }

So, after rebuilding the FlightGear binary, whenever you call the new "cool" API function, it will always return a "cool" string, which you could for example print out using a script like the following:

var result=cool();
print(result);

Argument Processing

static naRef f_cool (naContext c, naRef me, int argc, naRef* args)


The arguments to the function are passed in in the args array. The number of arguments is passed via the argc parameter (this is basically consistent with the standard signature of main in C/C++).

So, if you know that you require a certain number of arguments, you can also directly check argc for matching your requirements and show an error message, return nil, or throw an exception using naRuntimeError():

// Throw an error from the current call stack.  This function makes a
// longjmp call to a handler in naCall() and DOES NOT RETURN.  It is
// intended for use in library code that cannot otherwise report an
// error via the return value, and MUST be used carefully.  If in
// doubt, return naNil() as your error condition.  Works like
// printf().
void naRuntimeError(naContext c, const char* fmt, ...);


The "me" reference is set if the function was called as a method call on an object (e.g. object.your_function() instead of just your_function(), in which case "me" would be set to the object).

The naRef objects can be manipulated using the functions in nasal.h.

Basically, you can check the type of the reference with the following naIs*() functions:

int naIsNil(naRef r);
int naIsNum(naRef r);
int naIsString(naRef r);
int naIsScalar(naRef r);
int naIsVector(naRef r);
int naIsHash(naRef r);
int naIsCode(naRef r);
int naIsFunc(naRef r);
int naIsCCode(naRef r);

And then get the appropriate value out with things like naNumValue(), naStr_data(), etc...

So, if were now to change the interface of our earlier "cool" function in the example, to only return "cool" if we are passed at least one numerical parameter, and otherwise return "uncool", it could look like this:

static naRef f_cool (naContext c, naRef me, int argc, naRef* args)
{
    const char* cool="cool";
    const char* uncool="uncool";
    const char* res;
    naRef retval;
    if (!argc || !isNaNum(args[0]))
      result=uncool;
    else
      result=cool;     
    return naStr_fromdata(retval, result, strlen(result) );
}

You can test this, by running a simple Nasal script like the following:

# test new cool() function:
print( cool("foo") ); 
print( cool(100)   );


A common way to evaluate parameters passed to extension functions, looks like this:

static naRef f_foo (naContext c, naRef me, int argc, naRef* args) {
   naRef param1 = argc > 0 ? args[0] : naNil();
   naRef param2 = argc > 1 ? args[1] : naNil();
   naRef param3 = argc > 2 ? args[2] : naNil();
   //further parameter processing
   return naNil;
}

This will basically look for 3 parameters, and assign them accordingly - if they are not available, they will just be assigned nil using the naNil() calls (the next step would be to check if the parameters have the right types using the naIs* helpers).

Creating Nasal Data Structures

There are also functions to create Nasal data structures (hash tables, vectors, etc...) which you can return to the caller simply by returning the resulting naRef:

naRef naNil();
naRef naNum(double num);
naRef naNewString(naContext c);
naRef naNewVector(naContext c);
naRef naNewHash(naContext c);
naRef naNewFunc(naContext c, naRef code);
naRef naNewCCode(naContext c, naCFunction fptr);

String Manipulation API

// String utilities:
int naStr_len(naRef s);
char* naStr_data(naRef s);
naRef naStr_fromdata(naRef dst, const char* data, int len);
naRef naStr_concat(naRef dest, naRef s1, naRef s2);
naRef naStr_substr(naRef dest, naRef str, int start, int len);
naRef naInternSymbol(naRef sym);


Vector Manipulation API

// Vector utilities:
int naVec_size(naRef v);
naRef naVec_get(naRef v, int i);
void naVec_set(naRef vec, int i, naRef o);
int naVec_append(naRef vec, naRef o);
naRef naVec_removelast(naRef vec);
void naVec_setsize(naRef vec, int sz);


Hash Manipulation API

// Hash utilities:
int naHash_size(naRef h);
int naHash_get(naRef hash, naRef key, naRef* out);
naRef naHash_cget(naRef hash, char* key);
void naHash_set(naRef hash, naRef key, naRef val);
void naHash_cset(naRef hash, char* key, naRef val);
void naHash_delete(naRef hash, naRef key);
void naHash_keys(naRef dst, naRef hash);

// Retrieve the specified member from the object, respecting the
// "parents" array as for "object.field".  Returns zero for missing
// fields.
int naMember_get(naRef obj, naRef field, naRef* out);
int naMember_cget(naRef obj, const char* field, naRef* out);

Conversion & Comparison routines

Some useful conversion/comparison routines include:

int naEqual(naRef a, naRef b);
int naStrEqual(naRef a, naRef b);
int naTrue(naRef b);
naRef naNumValue(naRef n);
naRef naStringValue(naContext c, naRef n);

Passing Pointers to Nasal scripts

Things get more complicated if you need to pass a handle to a C/C++ object into a Nasal script. There, you need to use a wrapped handle type called a ghost ("Garbage-collectable Handle to an OutSide Thing"), which has a callback that you need to implement to deal with what happens when the Nasal interpreter garbage collects your object.

Ghost utilities:

typedef struct naGhostType {
    void(*destroy)(void*);
    const char* name;
} naGhostType;
naRef        naNewGhost(naContext c, naGhostType* t, void* ghost);
naGhostType* naGhost_type(naRef ghost);
void*        naGhost_ptr(naRef ghost);
int          naIsGhost(naRef r);

This needs to be specifically implemented for each new "ghost" type.

Things to avoid

And just for completeness: things get *really* complicated if you need to *store* a naRef you got from a Nasal script in a place where the interpreter can't see it.

There is a minimal API to handle this:

// "Save" this object in the context, preventing it (and objects
// referenced by it) from being garbage collected.
void naSave(naContext ctx, naRef obj);


But this API doesn't yet have a corresponding call to release a saved object. Just don't do this, basically. Always keep your Nasal data in Nasal space.

If you really need to do this, you may however want to check out the gcSave/gcRelease methods of the FGNasalSys class:

   // This mechanism is here to allow naRefs to be passed to
   // locations "outside" the interpreter.  Normally, such a
   // reference would be garbage collected unexpectedly.  By passing
   // it to gcSave and getting a key/handle, it can be cached in a
   // globals.__gcsave hash.  Be sure to release it with gcRelease
   // when done.
   int gcSave(naRef r);
   void gcRelease(int key);