Nasal/CppBind

From FlightGear wiki
Jump to navigation Jump to search
WIP.png Work in progress
This article or section will be worked on in the upcoming hours or days.
See history for the latest developments.
This article is a stub. You can help the wiki by expanding it.

Prerequisite

Intro

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

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++.

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.

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 Tom's Canvas system, there's now a new bindings framework to be found in simgear/nasal/cppbind. This is fully object oriented and supports modern C++ features.

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, #include'ing <simgear/nasal/cppbind>, uses boost templates to hide low level details.

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

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.

After working through this article, tome of the more straightforward things to play with i the beginning, would be exposing additional SG/FG classes to Nasal space, such as the SGSubsystem interface to register scripted SGSubsystems, or the autopilot system or exposing the random buildings system [1]. There's also a pending feature request (ticket #619) to implement USB-HID support [2].

For more technical Nasal questions (C API, internals etc), you'll probably want to refer to Philosopher, TheTom, Zakalawe or Hooray on the forum - TheTom and Zakalawe can also provide help on using cppbind, having both used it extensively during the last months.

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/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/Scripting, you'll want to look at the following sources that make use of cppbind (listed in ascending complexity):

  • NasalString.?xx
  • NasalHTTP.?xx
  • NasalCanvas.?xx
  • NasalPositioned_cppbind.?xx

Getting started

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

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

Next, create the NasalDemo.cxx source file:

// $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 x,y,z;
 void hello() {
  std::cout << "Hello World from CppBind!\n"; 
 }
};
 
// cppbind manages all objects as shared pointers, use boost::shared_ptr or SGReferenced objects
typedef boost::shared_ptr<Test> Test_ptr;
typedef nasal::Ghost< Test_ptr > NasalTest;
 
// next, two helper functions that tell cppbind how to
// convert our objects between C++  <-> Nasal
 
naRef to_nasal_helper(naContext c, Test *src)
  { 
    Test_ptr ptr(src); // set up a smart pointer wrapping src
    return NasalTest::create(c, ptr ); // return the smart pointer wrapped in a naGhost
  }
 
Test*
from_nasal_helper(naContext c, naRef ref, const Test*)
  { 
      return (Test*) naGhost_ptr(ref);
  }
 
 
 
// add this to FGNasalSys::init():
/*
 extern initNasalDemo(naRef globals, naContext c); // or add it to the header 

 if( !NasalDemo::isInit() )
 	initNasalDemo(_globals, _context);
*/
 
static naRef f_newtest(const nasal::CallContext& ctx)
{
  Test* t = new Test();
  // do some initial state setup
  t->x = 100;
  t->y = 200;
  t->z = 300;
  // and now return the new object to Nasal 
  return ctx.to_nasal( t );
}
 
 
// this will register our bindings in the Nasal engine
// it should  be called at the end of $FG_SRC/Scripting/NasalSys.cxx::FGNasalSys::init()
naRef initNasalDemo(naRef globals, naContext c)
{
    // This only needs to be called once for each ghost
    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
 
    // set up a  new namespace for our functions, named test
    nasal::Hash globals_module(globals, c),
              test = globals_module.createHash("test");
 
   // add an allocator to the test namespace for creating new test objects
   test.set("new", &f_newtest);
 
 
    return naNil(); //we already did all the namespace setup, so we can return naNil() here 
}

Testing the whole thing

Fire up the Nasal console, and run this:

# inspect the test namespace
debug.dump( test );
# inspect the result of running our allocator function
debug.dump( var t=test.new() );
 
# dump the x,y,z members to the console
 foreach(var e; ['x','y','z']) 
print ( t[e] );
 
# run the hello method
var t = test.new();
t.hello();

CallContext

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


Exposing C++ Classes

Returning Nasal classes to C++

Exposing static members

Supporting custom Types

  • to_nasal
  • from_nasal