Tracking unmanaged buffers and reading/writing single structs when using the Exchange C# API

If you’re using the Exchange C# API, you may find yourself creating and then releasing unmanaged memory buffers. For example, consider the API.A3DCrvEvaluate() call. Given a curve and it’s parameter value, this will evaluate and return the point (and optionally derivatives) on the curve.

To do this, you need to pass in an unmanaged memory buffer which Exchange will fill in with the result. So you need to create an unmanaged buffer, initialize it with one or more A3DVector3dData structs, call the API, get the structs out of the buffer and then release the buffer.

To make this easier, consider using the below BufferMgr class. You can create this with a byte size, in which case it will allocate the unmanaged buffer for you, or you can pass in an existing unmanaged buffer allocated elsewhere. Either way, when disposed, this frees the unmanaged buffer.

internal class BufferMgr : IDisposable
{
	public IntPtr Buffer { get; private set; }

	public BufferMgr(int size)
	{
		Buffer = Marshal.AllocHGlobal(size);
	}

	public BufferMgr(IntPtr buffer)
	{
		Buffer = buffer;
	}

	public void Dispose()
	{
		if (Buffer == IntPtr.Zero)
			return;

		Marshal.FreeHGlobal(Buffer);
		Buffer = IntPtr.Zero;
	}
}

With the C# “using var” pattern, this allows something very similar to C++ RAII as shown below.

using var buf = new BufferMgr(1000);
// access the buffer with buf.Buffer
// when buf goes out of scope, the unmanaged buffer is automatically freed

Going back to the example of API.A3DCrvEvaluate(). The Exchange C# API ships with some helper methods in Utils.cs which allow putting managed structs into and back out of unmanaged buffers. So you can use these together with the above, to do something like the below.

var ptOnCurveA3DData = new A3DVector3dData[2];
API.Initialize(out ptOnCurveA3DData[0]);
API.Initialize(out ptOnCurveA3DData[1]);
using var ptOnCurveA3DDataBufMgr = new BufferMgr(ArrayWrapper.WriteStructArray(ptOnCurveA3DData));
iRet = API.A3DCrvEvaluate(pEdge3DCurve, paramVal, 1, ptOnCurveA3DDataBufMgr.Buffer);
if (iRet != A3DStatus.A3D_SUCCESS)
	continue;
ptOnCurveA3DData = ArrayWrapper.ReadStructArray<A3DVector3dData>(ptOnCurveA3DDataBufMgr.Buffer, (uint)ptOnCurveA3DData.Length);
var ptOnCurveA3DWorld = ptOnCurveA3DData[0];
var tangetOnCurveA3DWorld = ptOnCurveA3DData[1];

As you can see the BufferMgr class avoids the need to manually free the buffer. In this case we use the constructor which takes a buffer as we want to use the buffer returned from the helper method which it allocates.

Additionally, sometimes you don’t have an array of structs but only a single struct. In this case the below helpers might be useful.

public static T ReadStruct<T>(IntPtr unmanagedArray)
{
	T result = default(T);

	if (unmanagedArray != IntPtr.Zero)
	{
		result = Marshal.PtrToStructure<T>(unmanagedArray);
	}

	return result;
}

public static IntPtr WriteStruct<T>(T value)
{
	var structSize = Marshal.SizeOf(typeof(T));
	IntPtr unmanagedBuffer = Marshal.AllocHGlobal(structSize);
	if (unmanagedBuffer == IntPtr.Zero)
		return IntPtr.Zero;
	Marshal.StructureToPtr(value, unmanagedBuffer, true);
	return unmanagedBuffer;
}

This could be used when evaluating the curve where you only want the point, not any derivatives as shown below.

A3DVector3dData minPtA3DData;
API.Initialize(out minPtA3DData);
using var minPtA3DDataBufMgr = new BufferMgr(ArrayWrapper.WriteStruct(minPtA3DData));
iRet = API.A3DCrvEvaluate(pAnalyticCurve, lineInterval.m_dMin, 0, minPtA3DDataBufMgr.Buffer);
if (iRet != A3DStatus.A3D_SUCCESS)
{
	logger.LogError($"unable to evaluate line at min param, {iRet}");
	return null;
}
minPtA3DData = ArrayWrapper.ReadStruct<A3DVector3dData>(minPtA3DDataBufMgr.Buffer);
1 Like