41. C#/.NET bindings of the AGX API

Some versions of AGX Dynamics will come with libraries which export the AGX Dynamics API to the .NET development environment (currently version 4.6.1). By utilizing swig (https://github.com/swig/swig), a large portion (and growing for each version) of the AGX API is exposed to use in .NET. This means that you can utilize the development environment from Microsoft and build C#/VB-based applications with physics from AGX.

The .NET wrapper comes with three layers of run time libraries. First is the C++ wrapper:

Library name

Description

agxDotNetRuntime.dll

AGX C++ .NET binding

Then the top layer which implements the actual .NET wrapper in C#:

Library name

Description

agxDotNet.dll

Interface library for using AGX in .NET

Then the math library:

Library name

Description

agxMathDotNet.dll

Math library implemented in C# for bridging between AGX and .NET

agxDotNet.dll and agxMathDotNet.dll are the interface libraries to be referenced in your typical .NET application. The C++/.NET binding library agxDotNetRuntime.dll will be automatically located according to the rules by which Windows locates dynamic libraries.

41.1. Functions

Since there are no free functions in C# (functions which are not members of a class), only classes with namespaces, the free functions that are available in the C++ API will get a somewhat different look. They will be added into an extra namespace (class). For example, the function agx::init() : As this namespace (agx) resides in the agxDotNet plugin, it will in C# be accessible as:

agx.agxSWIG.init();

This convention goes for all (class-less) functions found in the AGX API.

Also, some extensions methods are used to bridge agxDotNet.dll and agxMathDotNet.dll, which can be used like this:

using agx.extensions;

41.2. Sample application using the C# binding

41.2.1. testApplication

Below is a short code snippet, illustrating various API calls in the .NET version of the AGX programming API in C# code. For more examples, look into the directory: agx\swig\configuration\agxDotNet\testApplication

using agx.extensions;

class Program
{
 static void Main(string[] args)
 {

   // Try to use the Environment class.
   agxIO.FilePathContainer fp = agxIO.Environment.instance().getFilePath();
   string script = fp.find("data/python/tutorials/tutorial1_graphics.agxPy");
   Console.WriteLine("Script {0}", script);

   // This must be the first call to AGX (except for setting up the FilePath).
   // This is because the call to agx.init() (or in .NET: agx.agxSWIG.init())
   // will try to load plugins and
   // various resources. You have to specify where AGX can find these plugins,
   // license file etc.
   // See the user manual for more (important) information about this.
   //
   agx.agxSWIG.init();
   agx.agxSWIG.setNumThreads(3);

   {
     agxSDK.Simulation sim = new agxSDK.Simulation();
     sim.setDynamicsSystem(new agx.DynamicsSystem());

     agx.RigidBody body = new agx.RigidBody();
     body.setName("this is a body");
     body.setPosition(4, 0, 0);
     sim.add(body);

     var frame = new agx.Frame();
     frame.setLocalTranslate(2, 0, 0);
     var hinge = new agx.Hinge(body, frame);
     hinge.getMotor1D().setEnable(true);
     hinge.getMotor1D().setSpeed(0.1);
     sim.add(hinge);

     agx.RigidBodyRefVector bodies = sim.getDynamicsSystem().getRigidBodies();

     sim.stepForward();

     for (int i = 0; i < bodies.Count; ++i)
     {
       var bodyName = bodies[i].getName();
       Console.WriteLine("A body: " + bodyName);
     }

     agx.TimeGovernor tg = sim.getDynamicsSystem().getTimeGovernor();

     sim.remove(body);

     sim = null;
     body = null;
   }
   GC.Collect();
   GC.WaitForFullGCComplete();
   agx.agxSWIG.shutdown();
 }
}

41.2.2. CSharpViewer

CSharpViewer is located in the directory swig\SWIGDotNet\CSharpViewer. It is an example application which utilizes the agxOSG::ExampleApplication to create a graphics window and handle input like the agxViewer application.

With this application you can load a .agxPy script or a .agx file. All hinges and prismatic joints will be listed with a slider for each constraints’s Lock1D()/position controller. The range for each constraint will be set to [-PI,PI] for hinges and [-0.1,0.1] for prismatics. With the slider you can then control each constraints target position.

The sample application shows how to:

  • Create an application

  • Access objects in the simulation

  • Run and step the simulation through user events

This application can be built as a separate project:

  • Go to the directory

  • Start cmake-gui .

  • Choose correct build environment

  • Press Configure, Generate

  • Start visual studio with: devenv /useenv

  • Build the project. CSharpViewer.exe is the final result.

41.3. C#, SWIG and garbage generation

The C# bindings made by SWIG are created so that there are C# classes matching the C++ classes. For example there is a C# RigidBody class that matches the C++ agx::RigidBody. The SWIG-created C# RigidBody implementation is mostly a proxy object that holds a pointer and ownership information about the C++ RigidBody.

When methods in C# are called on the proxy object, P/Invoke is utilized to call native wrapper code generated by SWIG that converts the C# IntPtr held by the proxy object to the correct C++ pointer type and then calls the desired C++ code.

The flow from managed code in agxDotNet.dll, to native wrapper code in agxDotNetRuntime.dll to native agx and back is handled automatically and normally not something the user has to think about.

There is one drawback, when accessing C++ classes, C# proxy objects are generated and when those proxies are no longer needed, they will be garbage collected. The garbage collection can then cause unwated spikes and performance artifacts.

The AGX C# bindings has two different solutions to work around this problem with short lived proxy classes:

  • Simple datatypes such as e.g agx.Vec3 and agx.AffineMatrix4x4 are implemented as structs in C# and data marshalling is performed to convert to/from C++ datatypes to completely avoid proxy classes on the C# side.

  • Objects owned by C++ that are likely to be accessed often have had their proxy classes changed so that they are fetched from an object pool and can be returned to the object pool when no longer needed.

To revisit part of the short example above:

agx.RigidBodyRefVector bodies = sim.getDynamicsSystem().getRigidBodies();

for (int i = 0; i < bodies.Count; ++i)
{
  var bodyName = bodies[i].getName();
  Console.WriteLine("A body: " + bodyName);
}

Each iteration in the loop will create garbage. bodies[i] will lead to the creation of a agx.RigidBodyRef which will shortly there after be seen as garbage.

A more efficient way to perform the same thing would be

agx.RigidBodyRefVector bodies = sim.getDynamicsSystem().getRigidBodies();

for (int i = 0; i < bodies.Count; ++i)
{
  var bodyI = bodies[i];

  var bodyName = bodyI.getName();
  Console.WriteLine("A body: " + bodyName);

  bodyI.ReturnToPool(); // bodyI should be considered to be null now
                        // and no further methods should be invoked on the instance
}

The datatypes that are made poolable are:

  • agx.RigidBodyRef

  • agx.ConstraintRef

  • All constraints (Constraint, DistanceJoint, … )

  • All constraint controllers (Speed1D, Lock1D, Range1D, …)

  • agxCollide.GeometryRef

  • agxCollide.ShapeRef

  • All collision shapes (Box, Sphere, Mesh, …)

  • Contact data datatypes: * agxCollide.GeometryContact * agxCollide.ContactPointVector * agxCollide.ContactPoint

All the classes that are poolable implements the agx.IPoolable interface. That interface requires that three different methods should be implemented and of those three methods, only ReturnToPool should be used by users. The other two are needed by the ObjectPool to reconfigure the proxy object so that it can wrap a different C++ object without generating garbage.

If one needs to get access to one of the ObjectPool singletons that are created automatically in the background, one can access them via namespace.classname.GetObjectPool(), for example agx.RigidBodyRef.GetObjectPool().

The Object Pool has a property MaxPoolSize which controls the maximum number of empty proxies the pool should hold. The Object Pool also makes it possible to create empty proxies in advance via PreAllocate( int count ).

Normally, there should not be a big need to access to object pool directly, the important thing to remember is to call ReturnToPool() for poolable types when one is done with them.

41.4. Passing large Vectors to AGX

The C# interfaces for agx::Vector, agx::VectorPOD are wrapped by SWIG so that they should work in a similar way as C# Collections. For most operations this adds convenience and familiarity.

When passing large amounts of data, there is a performance downside due to the managing of data between C#/managed C++/unmanaged C++. This typically occurs for operations such as updating the heights of a agxTerrain.Terrain or a agxCollide.HeightField from C#.

Using the default functionality to set all elements in a agx.VectorPOD would perform managed -> unmanaged -> managed transitions for each element in the container. To resolve this problem and provide an efficient way to set the entire vector, the C# interface for AGX have augmented agx::VectorPOD<T> -based classes that use int- och float-based data types. The interface contains a method Set(T[] array) and a corresponding Get(T[] array) where the datatype T matches the VectorPOD being wrapped. Using these methods only one single managed -> unmanaged -> managed transition will be done when setting the entire vector instead of one for each element.

Below is a recommendation how to use the set method with an agx.RealVector, although the process can be used for any wrapped agx::VectorPOD class such as agx.Vec3Vector, agx.Vec2Vector, agx.UInt32Vector etc.

  1. Reuse an agx.RealVector

  2. Compute/set the data values in a native C# array

  3. Copy the data from the native C# array to the agx.RealVector with real_vector.Set( csharp_array )

The get method can be used in a similar way:

  1. Reuse a native C# array but make sure that it has the same size as the agx.RealVector.

  2. When you need to access the data in a agx.RealVector, copy the data from the agx.RealVector to the native C# array using real_vector.Get(csharp_array)

Note

While the Set method resizes the wrapped array to match the size of the C# array, the Get method will throw an ArgumentException if the sizes of the arrays do not match.

Below is a “complete” toy example using both the Set and Get methods:

// Create a C# array to copy into the agx.RealVector
double[] input = new double[100];
agx.RealVector agxVector = new agx.RealVector();

// Assign the elements in the input C# array
for ( int i = 0; i < input.Length; i++ )
  input[ i ] = (double)i;

// Copy the elements into the agx.RealVector
agxVector.Set( input );

// Create an output C# array with the same size as the agx.RealVector
double[] output = new double[agxVector.Count];

// Copy elements from the agx.RealVector into the C# array
agxVector.Get( output );

41.5. Building the SWIG binding

The sub-directory <agx-install-dir>\swig contains everything needed to build the C#/.NET binding. For information of how to build the libraries, read the README.txt in the SWIG directory.

41.6. Extending the C# interface

It is possible to extend the .NET interface by adding more classes/header files to the .i files found in the <agx>\swig\configuration sub-directory. For more details about SWIG’s interface files look at the SWIG documentation (https://github.com/swig/swig).

Adding another header file to SWIG can be done with the following steps:

  1. Locate the .h file which contain the class you want to export to .NET

  2. Identify which namespace the class/header file belongs to.

  1. For example agx::Constraint belongs to the agx namespace, then it should be included into agx.i

  1. Edit the interface file (for example agx.i) and add two include directives:

  1. Locate the use of the INCLUDE() macro. Add your header file: INCLUDE(namespace/headerfile.h) which will be added to the generated .cpp code.

  1. Follow the instruction in swig/README.txt to configure the wrapper project and build the binding libraries.

  2. To test that the new class exists, add code in program.cs that references the new class.

  3. When it works, copy the resulting dll files (agxDotNet.dll , agxDotNetRuntime.dll and agxMathDotNet.dll) to the path where you keep the AGX runtime libraries.