Nasal/CppBind: Difference between revisions

From FlightGear wiki
Jump to navigation Jump to search
m (not working on it ATM)
No edit summary
(33 intermediate revisions by 3 users not shown)
Line 1: Line 1:
<!-- {{WIP}} -->
<!-- {{WIP}} -->
{{Stub}}


== Prerequisite ==
{{Affected by|article=Deboosting FlightGear|work=Deboosting FlightGear}}
* [[Building FlightGear]]
* [[Developing using CMake]]
* [[Programming Resources|C++ experience]]
* [[Nasal|Nasal experience]]


== Intro ==
FlightGear's built-in [[Nasal]] scripting language comes with a set of standard libraries, and can be extended using FlightGear specific APIs.
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.
 
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.
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.
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, #include'ing <simgear/nasal/cppbind>, uses boost templates to hide low level details.
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 <tt>#include &lt;simgear/nasal/cppbind&gt;</tt> 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 - 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]].
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.
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.
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, some of the more useful things to play with i the beginning, would be exposing additional SG/FG classes to Nasal space, such as for example:
<!--
* the SGSubsystem interface to register scripted SGSubsystems
== Worthwhile Targets ==
* the autopilot system [http://forum.flightgear.org/viewtopic.php?p=149376#p149376] [http://forum.flightgear.org/viewtopic.php?f=66&t=21217&hilit=cppbind#p193357] [http://www.mail-archive.com/flightgear-devel@lists.sourceforge.net/msg38172.html] (there are certain [[How the Nasal GC works|Nasal GC issues]], so that we ask people not to implement FDM-coupled Nasal code like autopilots)
{{Template:Mentored Volunteer Effort
* exposing the sound manager, so that scripts can directly play audio files [http://www.mail-archive.com/flightgear-devel@lists.sourceforge.net/msg18550.html]
|mentors= Hooray (get in touch to learn more)
* exposing the random buildings system [http://forum.flightgear.org/viewtopic.php?f=5&t=21131&p=192489&hilit=cppbind#p192489]  
|skills=[[Building Flightgear]], [[Developing using CMake]], [[Programming Resources#Programming_-_Background_knowledge|C++]], some [[Nasal]] }}
* There's also a pending feature request ({{Issue|619}}) to implement USB-HID support [http://forum.flightgear.org/viewtopic.php?f=24&t=20159&p=185021&hilit=cppbind#p185021].
* [[Howto:Using OpenCL in FlightGear]]
* [[Developing with HLA|Nasal/HLA bindings]], so that we can run certain scripts as HLA federates outside the fgfs process space (such as bombable or local weather)


Before working on anything related, please do get in touch with other contributors to ensure that this list is still up-to-date.


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.
{{CppBind Ideas}}
-->
 
== Prerequisites ==
* [[Building FlightGear]]
* [[Developing using CMake]]
* [[Programming Resources|C++ experience]]
* [[Nasal|Nasal experience]]


== Objective ==
== 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.  
 
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 file|path=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.  
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):
Next, there are more sophisticated examples to be found in {{fg src file|path=src/Scripting}}, you'll want to look at the following sources that make use of cppbind (listed in ascending complexity):


* NasalString.?xx
* {{fg src file|path=src/Scripting/NasalString.cxx}}
* NasalHTTP.?xx
* {{fg src file|path=src/Scripting/NasalHTTP.cxx}}
* NasalCanvas.?xx
* {{fg src file|path=src/Scripting/NasalCanvas.cxx}}
* NasalPositioned_cppbind.?xx
* {{fg src file|path=src/Scripting/NasalPositioned_cppbind.cxx}}
 
== GHOSTs ==
{{See also|Howto:Extend_Nasal#Passing_pointers_to_Nasal_scripts}}
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.


== Getting started ==
== Getting started ==


Open $FG_SRC/Scripting/CMakeLists.txt and add these entries to the SOURCES/HEADER section respectively:
Open {{fg src file|path=src/Scripting/CMakeLists.txt}} and add these entries to the SOURCES/HEADER section respectively:
* NasalDemo.cxx (SOURCES)
* NasalDemo.cxx (SOURCES)
* NasalDemo.hxx (HEADERS)
* NasalDemo.hxx (HEADERS)
Line 70: Line 74:
</syntaxhighlight>
</syntaxhighlight>


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


<syntaxhighlight lang="cpp" enclose="div">
<syntaxhighlight lang="cpp" enclose="div">
if( !NasalDemo::isInit() )
initNasalDemo(_globals, _context);
initNasalDemo(_globals, _context);
</syntaxhighlight>
</syntaxhighlight>


Line 101: Line 104:
   
   
// cppbind manages all objects as shared pointers
// cppbind manages all objects as shared pointers
// use boost::shared_ptr or SGReferenced objects
// use std::shared_ptr or SGReferenced objects
// typically, you'll want to provide two helper
// typically, you'll want to provide two helper
// functions for each of your classes
// functions for each of your classes


typedef boost::shared_ptr<Test> Test_ptr;
typedef std::shared_ptr<Test> Test_ptr;
typedef nasal::Ghost< Test_ptr > NasalTest;
typedef nasal::Ghost< Test_ptr > NasalTest;
   
   
Line 146: Line 149:
// to call the code, add this to FGNasalSys::init():
// to call the code, add this to FGNasalSys::init():
/*
/*
if( !NasalDemo::isInit() )
  initNasalDemo(_globals, _context);
  initNasalDemo(_globals, _context);
*/
*/
Line 153: Line 154:
naRef initNasalDemo(naRef globals, naContext c)
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()
     // 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!)
     NasalTest::init("Test") // this is the ghost's symbol used in error messages/diagnostics (it is NOT the namespace/symbol used by nasal code!)

Revision as of 13:06, 14 June 2020


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.

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()

Exposing C++ Classes

Argument processing

Returning Nasal classes to C++

Inheritance

Exposing static members

Supporting custom Types

  • to_nasal
  • from_nasal