Looking for information related to the limits and odd behavior of out, [Out], and ref

  • Thread starter Thread starter Jacob Kemple
  • Start date Start date
J

Jacob Kemple

Guest
Hello there,

I happen to use heavy Interoperability features in some of my C# programs. In this process, I have found some odd behavior and limits to the out, ref, and [Out] options that exist. I also have found some work around tricks to some of the limitations.

Given this, I would like to ask about why the above examples behave the way they do. It would be nice to not have to use such tricks to work-around the limitation of using out or ref in params object[] signatures (or any params, actually). However, I am sure it is most likely a design reason.

I will try to illustrate the behavior I refer to in the pseudo code below, comments explain most of what goes on. Feel free to ask me more specifics.


[StructLayout(LayoutKind.Sequential)]
public struct Vector3
{
public int X;
public int Y;
public int Z;
}

public class InvokeDelegateTarget
{
public object[] Args { get; set; }
public Delegate Target { get; set; }
public object Result { get; set; }
}

// An example class showing limits on the use of out/ref
// in params signatures, as well as work-arounds to the limits.

public class InteropExamples
{
/* note: these delegates fields must be kept alive in a real class too,
or else GC can collect them.
*/

// One method has out, the other [Out] attribute (in order, A out, B [Out])
private readonly TryGetLocationA _tryGetLocationA;
private readonly TryGetLocationB _tryGetLocationB;

// One method has ref IntPtr, the other IntPtr[] (in order, A ref IntPtr, B IntPtr[])
private readonly IsEntityValidA _isEntityValidA;
private readonly IsEntityValidB _isEntityValidB;

private readonly int _mainThreadID;

public bool InvokeRequired => GetCurrentThreadId() != _mainThreadID;

public InteropExamples()
{
IntPtr addressOne = IntPtr.Zero; // real address of the func here of course.
IntPtr addressTwo = IntPtr.Zero; // real address of the func here of course.

_tryGetLocationA = Marshal.GetDelegateForFunctionPointer(addressOne, typeof (TryGetLocationA)) as TryGetLocationA;
_tryGetLocationB = Marshal.GetDelegateForFunctionPointer(addressOne, typeof (TryGetLocationB)) as TryGetLocationB;

_isEntityValidA = Marshal.GetDelegateForFunctionPointer(addressTwo, typeof (IsEntityValidA)) as IsEntityValidA;
_isEntityValidB = Marshal.GetDelegateForFunctionPointer(addressTwo, typeof (IsEntityValidB)) as IsEntityValidB;

_mainThreadID = System.Diagnostics.Process.GetCurrentProcess().Threads[0].Id;
}


// This way works as expected with no limits.
// delegate void TryGetLocationA(IntPtr address, out Vector3 result);
public Vector3 GetLocationA(IntPtr address)
{
Vector3 location;
// note: Invoke(_tryGetLocationA, address, out location) is not possible here.
_tryGetLocationA(address, out location);
return location;
}

// In this example, we use a trick to be able to
// use the out despite using params object[] args
// not allowing you to pass out or ref.
// delegate void TryGetLocationB(IntPtr address, [Out] Vector3[] result);
public Vector3 GetLocationB(IntPtr address)
{
// note: if your delegate signature -
// has out and not the attribute [Out]
// it will fail.
var location = new Vector3[1];
// The result gets passed to the first index of the array, 0.
// Use our special invoke method with params object[] thus not allowing -
// us to use out directly like in example A.
Invoke(_tryGetLocationB, address, location);
return location[0];
}

// This way works fine and as expected.
public bool IsEntityValid_A(IntPtr entityAddress)
{
// note: (bool) Invoke(_isEntityValidA, ref entityAddress) is not valid here.
return _isEntityValidA(ref entityAddress);
}

public bool IsEntityValid_B(IntPtr entityAddress)
{
// note: no ref tag needed here, unlike the [Out] example.
// The array trick allows us to once again by pass the limit -
// of not being able to use ref in params object[] (or params of any kind).
var isEntityValid = (bool) Invoke(_isEntityValidB, new[] {entityAddress});
return isEntityValid;
}


// An example of why we might want to invoke unmanaged funcs with -
// a generic method using params[] object[] args, which sadly -
// with out using tricks will not allow use of out/ref signatures.
// Work arounds exist and shown in this class.
private object Invoke<T>(T target, params object[] args) where T : class
{
var targetDelegate = target as Delegate;
if (targetDelegate == null)
{
throw new ArgumentException("Target method is not a delegate type.");
}

if (!InvokeRequired)
{
return targetDelegate.DynamicInvoke(args);
}

var callback = new InvokeDelegateTarget
{
Target = targetDelegate,
Args = args
};

/* notify some class to invoke your delegate */
Helper.ProcessDelegate(callback);
/* while callback.Result == null {} or what ever check you do */


return callback.Result;
}

[DllImport("kernel32.dll")]
private static extern int GetCurrentThreadId();

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void TryGetLocationB(IntPtr address, [Out] Vector3[] result);

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate void TryGetLocationA(IntPtr address, out Vector3 result);

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate bool IsEntityValidA(ref IntPtr entityAddress)

[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
delegate bool IsEntityValidB(IntPtr[] entityAddress)
}

Continue reading...
 
Back
Top