Nasal/CppBind

From FlightGear wiki
Jump to navigation Jump to search


IMPORTANT: Some, and possibly most, of the features/ideas discussed below are likely to be affected, and possibly even deprecated, by the ongoing work on Deboosting FlightGear. Please see: Post FlightGear 2020.2 LTS changes for further information

You are advised not to start working on anything directly related to this without first discussing/coordinating your ideas with other FlightGear contributors using the FlightGear developers mailing list. talk page.

FlightGear's built-in Nasal scripting language comes with a set of standard libraries, and can be extended using FlightGear specific APIs.

Exposing simulator internals to scripting space is a fairly common and useful thing, because it enables base package developers to access these internals without having to build FlightGear from source, so the barrier to entry is significantly lower and we've seen an increasing number of novel features purely implemented in scripting space, due to powerful APIs being available to aircraft developers and other base package developers.

Until FlightGear 2.8, the Nasal scripting engine only provided a C API to expose such hooks/bindings to scripting space or to expose scripting space data structures back to C/C++.

Unlike the core Nasal engine itself (which is C), FlightGear however is mostly written and being developed in C++. For quite a while, that meant that the Nasal APIs were a bit low-level, and sometimes also awkward to use when making functions, data structures or objects accessible between C++ and Nasal.

Thanks to development on Tom's Canvas system, there's now a new bindings framework to be found in $SG_SRC/simgear/nasal/cppbind. This is fully object oriented and supports modern C++ features by operating through classes and methods with full STL support, abstracting most common operations away.

You will find that most of the "old" code in $FG_SRC/Scripting still uses those old C-APIs for interacting with the Nasal engine. Only the new code, those that #include <simgear/nasal/cppbind> use boost templates to hide low level details.

Most of the code in the Nasal subsystem itself (FGNasalSys) also still uses the legacy C APIs. You will find the old, low-level APIs explained at Howto:Extend Nasal - this is just to explain the two approaches, to avoid unnecessary confusion.

The cppbind framework is much more generic and high level than the bare C APIs, cppbind includes unit testing support and makes use of modern C++ features like templates and STL support, including SimGear specific types like SGPath/SGGeod etc, its overhead is fairly small (not just performance, but also LoC to create new bindings). The cppbind framework is already extensively used by the Canvas system and the NasalPositioned_cppbind bindings, both of which are a good place to look for code examples.

Meanwhile, we suggest to favor cppbind over the old, low-level, approach, it isn't only much more elegant, but also saves you tons of typing, too - and will do certain error-checking automatically that you would otherwise have to implement manually.


Prerequisites

Objective

Provide a fully annotated step-by-step introduction to Nasal's cppbind framework. This is mostly based on existing code in SimGear/FlightGear. The cppbind framework itself is to be found $SG_SRC/simgear/nasal/cppbind and it's pretty well commented, and makes use of Doxygen strings. If you are already familiar with C++ and SG/FG,, you'll want to check out the unit tests in cppbind_test.cxx.

This write-up should get you started with the basics of the framework.

Next, there are more sophisticated examples to be found in $FG_SRC/src/Scripting, you'll want to look at the following sources that make use of cppbind (listed in ascending complexity):

GHOSTs

ghost (Garbage-collected Handle to OutSide Thingy – a virtual type that represents a C or C++ object).

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.

Ghosts were never intended to be ‘public’ first-order objects in Nasal, originally. The were more like a void* wrapper so you could safely pass a C pointer around, and pass it to native functions. If you look at props.Node uses them, with a Nasal wrapper, that was the original method.

James Turner added member support to allow them to be used publicly, since it avoids the overhead of the wrapper in many common cases, and Thomas G did some improvements to that. But at no point have we tried to systematically make Ghosts work as a first-order type [1]

Getting started

Open $FG_SRC/src/Scripting/CMakeLists.txt and add these entries to the SOURCES/HEADER section respectively:

  • NasalDemo.cxx (SOURCES)
  • NasalDemo.hxx (HEADERS)

Let's create the NasalDemo.hxx header file first:

// NasalDemo.hxx
#ifndef SCRIPTING_NASAL_DEMO_HXX
#define SCRIPTING_NASAL_DEMO_HXX
#include <simgear/nasal/nasal.h>
 
naRef initNasalDemo(naRef globals, naContext c);
#endif // of SCRIPTING_NASAL_DEMO_HXX

Next, open $FG_SRC/src/Scripting/NasalSys.cxx and locate the FGNasalSys::init() method, to call the new initNasalDemo() function, add this to the bottom of the function:

initNasalDemo(_globals, _context);

Next, we can create the NasalDemo.cxx file

#include <Main/globals.hxx>
#include <Main/util.hxx>

// $FG_SRC/Scripting/NasalDemo.cxx
 
#include <simgear/nasal/cppbind/from_nasal.hxx>
#include <simgear/nasal/cppbind/to_nasal.hxx>
#include <simgear/nasal/cppbind/NasalHash.hxx>
#include <simgear/nasal/cppbind/Ghost.hxx>
 
//the struct we want to expose to Nasal (could also be a class obviously)
struct Test {
 int value;
 void hello() {
  std::cout << "Hello World from CppBind!\nValue is:" << value ; 
 }
 void setValue(const int val) {value=val;}
 int getValue() const {return value;}
};
 
// cppbind manages all objects as shared pointers
// use std::shared_ptr or SGReferenced objects
// typically, you'll want to provide two helper
// functions for each of your classes

typedef std::shared_ptr<Test> Test_ptr;
typedef nasal::Ghost< Test_ptr > NasalTest;
 
// next, two helper functions that tell cppbind how to
// convert our objects when passing them between C++  <-> Nasal

// this will be used whenever you want to turn a C++ object into a Nasal Ghost 
naRef to_nasal_helper(naContext c, Test *obj)
  { 
    Test_ptr ptr(obj); // set up a smart pointer wrapping obj
    return NasalTest::create(c, ptr ); // return the smart pointer wrapped in a naGhost
  }
 

// and this will be used whenever you want to turn a Nasal Ghost
// into a C++ object
Test*
from_nasal_helper(naContext c, naRef ref, const Test*)
  { 
      return (Test*) naGhost_ptr(ref);
  }
 
 
// create a new Test object and returns it to Nasal
// as a naRef, wrapped in an naGhost
// 
static naRef f_newtest(const nasal::CallContext& ctx)
{
  Test* t = new Test();
  // we can do some initial state setup now
  t->value=100;
  // and now return the new object to Nasal 
  return ctx.to_nasal( t ); // NOTE: this only calls to_nasal - the to_nasal_helper we provided above is internally used
}
 
 
// this will register our bindings in the Nasal engine
// it should  be called at the end of $FG_SRC/Scripting/NasalSys.cxx::FGNasalSys::init()
// 
// to call the code, add this to FGNasalSys::init():
/*
 	initNasalDemo(_globals, _context);
*/

naRef initNasalDemo(naRef globals, naContext c)
{
   if(NasalTest::isInit() ) return naNil(); // avoid re-init during reset/re-init

    // This only needs to be called once for each ghost, so make sure to use the ::isInit() check in FGNasalSys::init()
    NasalTest::init("Test") // this is the ghost's symbol used in error messages/diagnostics (it is NOT the namespace/symbol used by nasal code!)
                .method("hello", &Test::hello) // add a method to the ghost and map it to the method in the struct/class
		.member("value", &Test::getValue, &Test::setValue); 
    // set up a  new namespace for our functions, named test
    nasal::Hash globals_module(globals, c),
              test = globals_module.createHash("test"); // this is the namespace we'll see and use in Nasal
 
   // add an allocator to the test namespace for creating new test objects
   // which will be accessible as test.new()
   test.set("new", &f_newtest);
 
 
    return naNil(); //we already did all the namespace setup, so we can simply return naNil() here, it's discarded anyways 
}

Testing the whole thing

Rebuild & run FlightGear, and then fire up the Nasal Console, and run this:

# inspect our new test namespace
debug.dump( test );

# create a test object:
var obj = test.new();

# inspect the result of running our allocator function
debug.dump( obj );
 
# run the hello method
obj.hello();

# print the value
print( obj.value );

# change the value
obj.value = 4444;

# print it again
print( obj.value );

Exposing HLA classes to Nasal

Here's another stub, this time exposing the simgear::HLAFederate class and a handful of its methods to Nasal space as a Nasal Ghost. Note that the following snippet assumes, that:

  • You configured, built & installed OpenRTI (see HLA)
  • you configured, built & installed SimGear with -DENABLE_RTI=ON
  • you configured, built FlightGear with -DENABLE_RTI=ON

In and of itself, this snippet isn't yet particularly useful, especially because we don't want HLA federates to be running inside the main process, but the same HLA bindings could obviously also be used by a standalone Nasal interpreter at some point. So this just serves as an example. To learn more about HLA, please also see Developing with HLA and Nasal HLA standalone.

// $FG_SRC/Scripting/NasalHLA.cxx

#ifdef HAVE_CONFIG_H
#  include "config.h"
#endif

#include <Main/globals.hxx>
#include <Main/util.hxx>
 
#include <simgear/nasal/cppbind/from_nasal.hxx>
#include <simgear/nasal/cppbind/to_nasal.hxx>
#include <simgear/nasal/cppbind/NasalHash.hxx>
#include <simgear/nasal/cppbind/Ghost.hxx>

#include <simgear/hla/HLAFederate.hxx>
 
typedef boost::shared_ptr<simgear::HLAFederate> HLAFederate_ptr;
typedef nasal::Ghost< HLAFederate_ptr > NasalHLAFederate;

naRef to_nasal_helper(naContext c, simgear::HLAFederate* obj)
{
	HLAFederate_ptr ptr(obj);
	return NasalHLAFederate::create(c, ptr);
}
 
simgear::HLAFederate*
from_nasal_helper(naContext c, naRef ref, const simgear::HLAFederate*)
{
	return (simgear::HLAFederate*) naGhost_ptr(ref);
}

typedef boost::shared_ptr<simgear::HLAObjectClass> HLAObjectClass_ptr;
typedef nasal::Ghost< HLAObjectClass_ptr > NasalHLAObjectClass;

naRef to_nasal_helper(naContext c, simgear::HLAObjectClass* obj)
{
	HLAObjectClass_ptr ptr(obj);
	return NasalHLAObjectClass::create(c, ptr);
}
 
simgear::HLAObjectClass*
from_nasal_helper(naContext c, naRef ref, const simgear::HLAObjectClass*)
{
	return (simgear::HLAObjectClass*) naGhost_ptr(ref);
}

static naRef f_new_federate(const nasal::CallContext& ctx)
{
  
  return ctx.to_nasal( new  simgear::HLAFederate );
}
 

naRef initNasalHLA(naRef globals, naContext c)
{
    nasal::Hash globals_module(globals, c);
 
   using namespace simgear;
   NasalHLAFederate::init("hla.federate")
	.method("init", &HLAFederate::init) 
	.method("update", &HLAFederate::update)
	.method("shutdown", &HLAFederate::shutdown)
	.method("createObjectClass", &HLAFederate::createObjectClass)
	.method("setFederateType", &HLAFederate::setFederateType)
	.method("setFederationExecutionName", &HLAFederate::setFederationExecutionName);

   nasal::Hash hla = globals_module.createHash("hla");
   hla.set("new", &f_new_federate); 
   return naNil();
}

CallContext

  • isNumeric()
  • isString()
  • isHash()
  • isVector()
  • isGhost()
  • requireArg<type>(index)
  • getArg()
  • popFront()
  • popBack()