.NET versus C++ API

The .NET framework implies differences between the C++ and the .NET API. This section may be skipped by .NET-only developers. However, it may be helpful if you are already familiar with the Open Inventor C++ API.



Classes and Extensions not Available in Open Inventor .NET

  • The DialogViz classes provided with Open Inventor C++ are not available because you will use the standard Winforms package instead.
  • The GDI output available with HardCopy C++ is not currently available in the HardCopy package of Open Inventor .NET.
  • The TerrainViz extension of Open Inventor C++ is not currently available with Open Inventor .NET.
  • The MeshViz XLM extension is not currently available with Open Inventor .NET.

C++ Features Available in .NET But With a Different Interface

The following features cannot be implemented in the same way as they are implemented in C++.

Callback mechanism

The callback mechanism is used very often by Open inventor C++. Callback functions are defined as follows:

typedef <return_type> functionCB(void * userData, type1 arg1,..., typen argn);
static  <return_type> myFunctionCB(void * userData, type1 arg1,..., typen argn)
{
...
}

The callback is registered by calling an addCallback method:

 addCallback((functionCB*) myFunctionCB, this);

The .NET implementation is based on delegates and each type of callback is implemented by a specific delegate.
The following example shows how to set a callback on a selection point event.

bool SelectionPointCB(SoAction action, SoPrimitiveVertex v)
{
  SoDetail detail = v.GetDetail();
  int    coordIndex = ((SoPointDetail) detail).GetCoordinateIndex();
  ...
  return false; // con tinue examining next primitives
}

The delegate is registered as follows:

 SoExtSelection selection = new SoExtSelection();
selection.PointFilterCallback = new SoExtSelection.PointCB(SelectionPointCB);

Pointers, values

Most objects are handled through references -- not pointers or values, but only references. However, struct types (e.g. SbVec3f)are passed by value.

C++

SoCone *cone = new SoC one;
cone->height.setValue( 3 );

C#

SoCone cone = new SoCone();
 cone.height.SetValue( 3 ); // note also difference in capitalization of method name

Another minor difference between C++ and C# involves the new operator. The example below shows setting coordinate values into the vertex field of an SoVertexProperty node.

C++

Node->vertex.se t1Value (0, SbVec3f(x,y,z));

C#

Node.vertex.Set 1Value (0, new SbVec3f(x,y,z)); // "new" is required

C++ Features That Are Not Necessary in .NET

  • The C++ methods SoBase::ref(), SoBase::unref(), and SoBase::unrefNoDelete() are not needed because reference counting is a feature of the .NET language.
  • Class SoType is not needed because .NET Type is equivalent.
  • Class SbString is not needed because .NET String is equivalent.
  • Class SbBool is not needed because .NET bool is equivalent.
  • The C++ methods SoDB::init(), SoInteraction::init(), and SoNodeKit::init() are not needed because these classes are automatically initialized when loading the first Open Inventor.NET class. Likewise for SoHardCopy::init(), SoVolumeRendering::init(), and PoMeshviz::init().

Writing Custom Nodes

The Open Inventor .NET framework offers two possibilities to write custom nodes:

  • Wrapping C++ custom nodes. This first approach is dedicated to developpers who want to keep the core engine of their application written in C++ but prefers .NET for other development aspects.
  • Writing your custom node in a .NET-compliant language. No more needs to write a C++ node and write a wrapper for it in using intermediate language.

Writing the custom node in C++

The first step is to write your custom node in the usual way, as if you were extending the C++ Open Inventor API. Extended C++ nodes must be built in a separate DLL that exports derived classes and overridden methods.

The following files, Pyramid.h and Pyramid.cpp, illustrate a custom C++ geometry node that draws a pyramid.

Writing the custom node wrapper class

Most of time, the managed class has the same name as the native class. To avoid class name conflicts, all managed classes must be fully qualified except the class you're writing or classes in the same namespace. Do not use the "using namespace" directive in your source file if it already includes a native header containing a class contained in the managed namespace.

...
#include <Inventor/nodes/SoSeparator.h>
using namespace OIV.Inventor.Nodes;
...

namespace CustomNodes {
//---------------------------------------------------------
void MyCustomNode::MyMethod() {
  ...
  SoSeparator *newSeparator = new SoSeparator(); // Error: could be native SoSeparator or managed SoSeparator
  ...
}
...
} // CustomNodes namespace end
...
#include <Inventor/nodes/SoSeparator.h>
...

namespace CustomNodes {
//---------------------------------------------------------
void MyCustomNode::MyMethod() {
  ...
  OIV::Inventor::Nodes::SoSeparator^ managedSeparator = gcnew OIV::Inventor::Nodes::SoSeparator(); // Ok no conflict
  ::SoSeparator *nativeSeparator = new ::SoSeparator(); // Ok no conflict
  ...
}
...
} // CustomNodes namespace end

Native class must not be defined within a namespace.

The managed class must be written using the managed C++ syntax (not C++/CLI syntax). If your application is entirely written in C#, you must create a separate assembly for your custom nodes.

Constructors

Two constructors required by the Open Inventor .NET kernel module must be defined in your custom node with "public protected" visibility. The signatures are:

  • MyCustomNode(System::IntPtr obj, OIV::Inventor::SoNetBase::ConstructorCommand c);
  • MyCustomNode(OIV::Inventor::SoNetBase::ConstructorCommand c);

The default minimal implementation must be the following:

//---------------------------------------------------------
MyCustomNode::MyCustomNode(System::IntPtr obj, OIV::Inventor::SoNetBase::ConstructorCommand c) : 
  OIV::Inventor::Nodes::TheMotherClass(OIV::Inventor::SoNetBase::ConstructorCommand::DontCreateInstance) 
{
  ::MyCustomNode* p0_handle =  (::MyCustomNode *) obj.ToPointer();
  SetUnmanagedHandle(p0_handle);
  InitializeManagedFields();
}

//---------------------------------------------------------
MyCustomNode::MyCustomNode(OIV::Inventor::SoNetBase::ConstructorCommand c) : 
  OIV::Inventor::Nodes::TheMotherClass(c) 
{

}

::MyCustomNode represents the native class. TheMotherClass is the mother class name of the managed class.

The SetUnmanagedHandle method sets the native pointer.

The InitializeManagedFields() method instantiates the additional fields of the class. See the next section for further details.

In this context, the class cannot be instantiated outside the assembly (remember the "public protected" visibility of the two first constructors). These constructors are only used by Open Inventor .NET kernel module to instantiate a managed object from a native object.

You can define public constructors in order for your class to be accessible outside the assembly.

//---------------------------------------------------------
MyCustomNode::MyCustomNode() : 
  OIV::Inventor::Nodes::TheMotherClass(OIV::Inventor::SoNetBase::ConstructorCommand::DontCreateInstance)
{
  System::Threading::Monitor::Enter(OIV::Inventor::SoNetBase::ConstructorMutex);
  ::MyCustomNode* p0_handle =  new ::MyCustomNode();
  System::Threading::Monitor::Exit(OIV::Inventor::SoNetBase::ConstructorMutex);
  // The last flag set to 'true' indicates that managed object is responsible 
  // for deleting native object.
  SetUnmanagedHandle(p0_handle, true);
  InitializeManagedFields();
}

The constructor is responsible for instantiating the native object, setting the native pointer, and initializing additional fields.

You should lock the global constructor mutex when creating the native object (required for database consistency since garbage collector runs in a separate thread).

Fields

Additional fields must be defined in the "public" visibility section of the new class. All available fields are in namespace OIV.Inventor.Fields. For the Pyramid, we need to define additional fields; the native counterpart fields are already defined in the native Pyramid node.

public: 

  // parts
  OIV::Inventor::Fields::SoSFBitMask^ parts;

  // baseWidth
  OIV::Inventor::Fields::SoSFFloat^ baseWidth;

  // baseDepth
  OIV::Inventor::Fields::SoSFFloat^ baseDepth;

  // height
  OIV::Inventor::Fields::SoSFFloat^ height;
InitializeManagedFields method

The managed fields must be created from the native fields. The InitializeManagedFields creates links.

//---------------------------------------------------------
void Pyramid::InitializeManagedFields() {

  parts     = static_cast<OIV::Inventor::Fields::SoSFBitMask^>
                (OIV::Inventor::PanObjectManager::CreateManagedObjectFor(
                   &(((::Pyramid*) UnmanagedHandle.ToPointer())->parts),
                   OIV::Inventor::Fields::SoSFBitMask::typeid, false));
  baseWidth = static_cast<OIV::Inventor::Fields::SoSFFloat^>
                (OIV::Inventor::PanObjectManager::CreateManagedObjectFor(
                   &(((::Pyramid*) UnmanagedHandle.ToPointer())->baseWidth),
                   OIV::Inventor::Fields::SoSFFloat::typeid, false));
  baseDepth = static_cast<OIV::Inventor::Fields::SoSFFloat^>
                (OIV::Inventor::PanObjectManager::CreateManagedObjectFor(
                   &(((::Pyramid*) UnmanagedHandle.ToPointer())->baseDepth),
                   OIV::Inventor::Fields::SoSFFloat::typeid, false));
  height    = static_cast<OIV::Inventor::Fields::SoSFFloat^>
                (OIV::Inventor::PanObjectManager::CreateManagedObjectFor(
                   &(((::Pyramid*) UnmanagedHandle.ToPointer())->height),
                   OIV::Inventor::Fields::SoSFFloat::typeid, false));

  // Do not forget to call mother class method!
  OIV::Inventor::Nodes::SoShape::InitializeManagedFields();
}

The CreateManagedObjectFor instantiates a managed object from an unmanaged (native) object.

  • The first parameter is the native pointer of field,
  • The second parameter is the managed counterpart class type,
  • The last parameter indicates if the managed object is responsible for the native object's destruction when the managed object is garbage collected. In this case, fields are on the class stack and they are freed when the native object is deleted. The managed fields must not delete the native fields.

Class initialization

What about the native class init? Before instantiating any native node, the InitClass() method must be called.

Defining an InitClass method with private access

This method will be responsible for calling the native InitClass()

::Pyramid::InitClass();

This method will also create a link between the native type and the managed type. Most of the native classes define the getClassTypeId() method. We use the string representation of the native class to create the link. The PanObjectManager::RegisterClassLink method must be called with the following parameters:

  • The first parameter is the string representation of the native class,
  • The second parameter is the type of the pending managed class.
OIV::Inventor::PanObjectManager::RegisterClassLink( ::Pyramid::getClassTypeId().getName().getString(),
                                                    Pyramid::typeid );

You can perform additional initialization as required by your node.

Defining a static constructor to call the InitClass method

The .NET framework allows you to define static constructors that are called once when the first instance of the class is created. The static constructor will call the InitClass() method previously defined.

public :

static Pyramid() 
{	
  InitClass();
} 

Class ready to use

Once you have built the assembly containing the new node, you can use it just like existing Open Inventor nodes in your application. References to InventorNet.dll must be added or updated for the PyramidNet and PyramidNetTest projects.