Added AnySizeStringMarshaler<T> which is an IVanaraMarshaler implementation to handle structures where the last field is a single character string array. Also added ability to use "*" as field name to indicate that string or array length should be determined by the amount of allocated memory.

pull/213/head
dahall 2021-02-18 14:22:59 -07:00
parent f13930bd63
commit 87664c5f97
2 changed files with 246 additions and 7 deletions

View File

@ -20,7 +20,7 @@ namespace Vanara.InteropServices
/// <param name="value">The initial value of the structure, if provided.</param>
/// <param name="sizeFieldName">
/// The name of the field in <typeparamref name="T"/> that holds the length of the array. If <see langword="null"/>, the first
/// public field will be selected.
/// public field will be selected. If "*", then the array size will be determined by the amount of allocated memory.
/// </param>
/// <exception cref="InvalidOperationException">This class can only manange sequential layout structures.</exception>
public SafeAnysizeStruct(in T value, string sizeFieldName = null) : base(baseSz)
@ -34,7 +34,7 @@ namespace Vanara.InteropServices
/// <param name="size">The size of the allocated memory in <paramref name="allocatedMemory"/> in bytes.</param>
/// <param name="sizeFieldName">
/// The name of the field in <typeparamref name="T"/> that holds the length of the array. If <see langword="null"/>, the first
/// public field will be selected.
/// public field will be selected. If "*", then the array size will be determined by the amount of allocated memory.
/// </param>
public SafeAnysizeStruct(IntPtr allocatedMemory, SizeT size, string sizeFieldName = null) : base(allocatedMemory, size, false)
{
@ -65,12 +65,14 @@ namespace Vanara.InteropServices
#if !(NET20 || NET35 || NET40)
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
#endif
protected override int GetArrayLength(in T local) => Convert.ToInt32(fiCount.GetValue(local));
protected override int GetArrayLength(in T local) => fiCount is null ? GetArrLenFromSz() : Convert.ToInt32(fiCount.GetValue(local));
private int GetArrLenFromSz() => 1 + (Size - baseSz) / Marshal.SizeOf(elemType);
private void InitCountField(string sizeFieldName)
{
fiCount = string.IsNullOrEmpty(sizeFieldName) ? structType.GetOrderedFields().First() : structType.GetField(sizeFieldName, BindingFlags.Public | BindingFlags.Instance);
if (fiCount is null) throw new ArgumentException("Invalid size field name.", nameof(sizeFieldName));
fiCount = string.IsNullOrEmpty(sizeFieldName) ? structType.GetOrderedFields(binds).First() : structType.GetField(sizeFieldName, binds);
if (fiCount is null && sizeFieldName != "*") throw new ArgumentException("Invalid size field name.", nameof(sizeFieldName));
}
}
@ -81,6 +83,8 @@ namespace Vanara.InteropServices
/// <typeparam name="T">The type of the structure.</typeparam>
public abstract class SafeAnysizeStructBase<T> : SafeMemoryHandle<CoTaskMemoryMethods>
{
internal const BindingFlags binds = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
/// <summary>The base, unextended size of T.</summary>
protected static readonly int baseSz;
@ -96,7 +100,7 @@ namespace Vanara.InteropServices
if (!structType.IsLayoutSequential)
throw new InvalidOperationException("This class can only manange sequential layout structures.");
baseSz = Marshal.SizeOf(structType);
fiArray = structType.GetOrderedFields().Last();
fiArray = structType.GetOrderedFields(binds).Last();
if (!fiArray.FieldType.IsArray)
throw new ArgumentException("The field information must be for an array.", nameof(fiArray));
elemType = fiArray.FieldType.FindElementType();
@ -202,4 +206,112 @@ namespace Vanara.InteropServices
return s.Value;
}
}
/// <summary>
/// A marshaler implementation of <see cref="IVanaraMarshaler"/> to marshal structures whose last field is a character array of length
/// (1) and that uses a field to determine the length of the full string.
/// <para>
/// Use the cookie paramter of <see cref="AnySizeStringMarshaler{T}"/> to specify the name of the field in <typeparamref name="T"/> that
/// specifies the length of the string in the last field of <typeparamref name="T"/> along with use indicators.
/// </para>
/// <para>
/// If the field specifies byte, rather than character length, follow the field name with a colon (:) followed by 'b' (for bytes) or 'c'
/// (for characters).
/// </para>
/// <para>
/// If the field specifies a length that does NOT include the NULL terminator, follow the field name, colon (:), and type specifier by
/// 'r' (for raw) or 'n' (for null-terminated).
/// </para>
/// <para>If the field name is "*", then the string length will be determined by the amount of allocated memory.</para>
/// </summary>
/// <typeparam name="T">The structure type to be marshaled.</typeparam>
public class AnySizeStringMarshaler<T> : IVanaraMarshaler
{
private static readonly int charSz;
private static readonly int baseSz;
private readonly FieldInfo fiCount;
private static readonly FieldInfo fiArray;
private static int strOffset;
private readonly bool cntIsBytes = false;
private readonly bool cntInclNull = true;
private readonly bool allMem = false;
static AnySizeStringMarshaler()
{
var structType = typeof(T);
if (!structType.IsLayoutSequential)
throw new InvalidOperationException("This class can only manange sequential layout structures.");
baseSz = Marshal.SizeOf(structType);
fiArray = structType.GetOrderedFields(SafeAnysizeStructBase<T>.binds).Last();
if (fiArray.FieldType != typeof(string))
throw new ArgumentException("The field information must be for a string.", nameof(fiArray));
charSz = StringHelper.GetCharSize(typeof(T).StructLayoutAttribute.CharSet);
strOffset = Marshal.OffsetOf(structType, fiArray.Name).ToInt32();// unchecked((uint)Marshal.ReadInt32(fiArray.FieldHandle.Value.Offset(4 + IntPtr.Size))) & 0x7FFFFFF;
}
/// <summary>Initializes a new instance of the <see cref="AnySizeStringMarshaler{T}"/> class.</summary>
/// <param name="cookie">
/// The name of the field in <typeparamref name="T"/> that specifies the number of elements in the last field of <typeparamref name="T"/>.
/// <para>
/// If the field specifies byte, rather than character length, follow the field name with a colon (:) followed by 'b' (for bytes) or
/// 'c' (for characters).
/// </para>
/// <para>
/// If the field specifies a length that does NOT include the NULL terminator, follow the field name, colon (:), and type specifier
/// by 'r' (for raw) or 'n' (for null-terminated).
/// </para>
/// </param>
public AnySizeStringMarshaler(string cookie)
{
if (string.IsNullOrEmpty(cookie))
fiCount = typeof(T).GetOrderedFields(SafeAnysizeStructBase<T>.binds).First();
else if (cookie == "*")
allMem = true;
else
{
var parts = cookie.Split(':');
if (parts.Length == 2 && parts[1].Length >= 2)
{
cntIsBytes = parts[1][0] == 'b';
cntInclNull = parts[1].Length > 1 && parts[1][1] == 'n';
}
fiCount = typeof(T).GetField(parts[0], SafeAnysizeStructBase<T>.binds);
}
if (fiCount is null && !allMem) throw new ArgumentException("Invalid string length field name.", nameof(cookie));
}
SizeT IVanaraMarshaler.GetNativeSize() => baseSz;
SafeAllocatedMemoryHandle IVanaraMarshaler.MarshalManagedToNative(object managedObject)
{
var h = managedObject is null ? SafeCoTaskMemHandle.Null : new SafeCoTaskMemHandle(baseSz);
var str = fiArray.GetValue(managedObject) as string;
var len = allMem ? StringHelper.GetByteCount(str, true, typeof(T).StructLayoutAttribute.CharSet) : Convert.ToInt32(fiCount.GetValue(managedObject));
var clen = cntIsBytes ? len / charSz : len;
if (cntInclNull && !allMem) --clen;
if (str is not null && str.Length > clen)
str = str.Substring(0, clen);
h.Size += StringHelper.GetByteCount(str, cntInclNull, typeof(T).StructLayoutAttribute.CharSet) - charSz;
Marshal.StructureToPtr(managedObject, h, false);
StringHelper.Write(str, ((IntPtr)h).Offset(strOffset), out _, cntInclNull, typeof(T).StructLayoutAttribute.CharSet);
return h;
}
object IVanaraMarshaler.MarshalNativeToManaged(IntPtr pNativeData, SizeT allocatedBytes)
{
if (pNativeData == IntPtr.Zero) return null;
var o = Marshal.PtrToStructure(pNativeData, typeof(T));
int len;
if (allMem)
len = (allocatedBytes - strOffset) / charSz;
else
{
len = Convert.ToInt32(fiCount.GetValue(o));
if (cntIsBytes) len /= charSz;
if (cntInclNull) --len;
}
fiArray.SetValue(o, StringHelper.GetString(pNativeData.Offset(strOffset), len, typeof(T).StructLayoutAttribute.CharSet));
return o;
}
}
}

View File

@ -16,6 +16,22 @@ namespace Vanara.InteropServices.Tests
[TestFixture()]
public class AnysizeStructTests
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct TestStrStructU
{
public int iVal;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1)]
public string array;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct TestStrStructA
{
public int iVal;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1)]
public string array;
}
[StructLayout(LayoutKind.Sequential)]
public struct TestStruct
{
@ -27,7 +43,7 @@ namespace Vanara.InteropServices.Tests
}
[Test]
public void ConvertTest()
public void StructNamedFieldTest()
{
var array = new[] { long.MinValue, 1L, long.MaxValue };
var ts = new TestStruct { iVal = array.Length, array = array };
@ -41,5 +57,116 @@ namespace Vanara.InteropServices.Tests
Assert.That(tsout.Value.iVal, Is.EqualTo(3));
Assert.That(((TestStruct)tsout).array, Is.EquivalentTo(array));
}
[Test]
public void StructAssumedFieldTest()
{
var array = new[] { long.MinValue, 1L, long.MaxValue };
var ts = new TestStruct { iVal = array.Length, array = array };
using var mem = new SafeAnysizeStruct<TestStruct>(ts);
Assert.That((int)mem.Size, Is.EqualTo(32));
Assert.That(mem.Value.iVal, Is.EqualTo(3));
Assert.That(mem.Value.array, Is.EquivalentTo(array));
var tsout = new SafeAnysizeStruct<TestStruct>(mem, mem.Size, null);
Assert.That(tsout.Value.iVal, Is.EqualTo(3));
Assert.That(((TestStruct)tsout).array, Is.EquivalentTo(array));
}
[Test]
public void StructNoFieldTest()
{
var array = new[] { long.MinValue, 1L, long.MaxValue };
var ts = new TestStruct { iVal = array.Length, array = array };
using var mem = new SafeAnysizeStruct<TestStruct>(ts);
Assert.That((int)mem.Size, Is.EqualTo(32));
Assert.That(mem.Value.iVal, Is.EqualTo(3));
Assert.That(mem.Value.array, Is.EquivalentTo(array));
var tsout = new SafeAnysizeStruct<TestStruct>(mem, mem.Size, "*");
Assert.That(tsout.Value.iVal, Is.EqualTo(3));
Assert.That(((TestStruct)tsout).array, Is.EquivalentTo(array));
}
[Test]
public void StringNamedFieldTest()
{
var ts = new TestStrStructU { iVal = str.Length + 1, array = str };
var strOffset = Marshal.OffsetOf<TestStrStructU>(nameof(TestStrStructU.array)).ToInt64();
IVanaraMarshaler m = new AnySizeStringMarshaler<TestStrStructU>(nameof(TestStrStructU.iVal));
using (var mem = m.MarshalManagedToNative(ts))
{
Assert.That((long)mem.Size, Is.GreaterThanOrEqualTo(strOffset + (str.Length + 1) * StringHelper.GetCharSize(CharSet.Unicode)));
Assert.That(StringHelper.GetString(((IntPtr)mem).Offset(strOffset), CharSet.Unicode), Is.EqualTo(str));
var tsout = (TestStrStructU)m.MarshalNativeToManaged(mem, mem.Size);
Assert.That(tsout.iVal, Is.EqualTo(ts.iVal));
Assert.That(tsout.array, Is.EquivalentTo(str));
}
m = new AnySizeStringMarshaler<TestStrStructU>(nameof(TestStrStructU.iVal) + ":br");
using (var mem = m.MarshalManagedToNative(ts))
{
var newStr = str.Substring(0, ts.iVal / StringHelper.GetCharSize(CharSet.Unicode));
Assert.That((long)mem.Size, Is.GreaterThanOrEqualTo(strOffset + ts.iVal));
Assert.That(StringHelper.GetString(((IntPtr)mem).Offset(strOffset), CharSet.Unicode), Is.EqualTo(newStr));
var tsout = (TestStrStructU)m.MarshalNativeToManaged(mem, mem.Size);
Assert.That(tsout.iVal, Is.EqualTo(ts.iVal));
Assert.That(tsout.array, Is.EquivalentTo(newStr));
}
}
[Test]
public void StringNamedFieldATest()
{
var ts = new TestStrStructA { iVal = str.Length + 1, array = str };
var strOffset = Marshal.OffsetOf<TestStrStructA>(nameof(TestStrStructA.array)).ToInt64();
IVanaraMarshaler m = new AnySizeStringMarshaler<TestStrStructA>(nameof(TestStrStructA.iVal));
using (var mem = m.MarshalManagedToNative(ts))
{
Assert.That((long)mem.Size, Is.GreaterThanOrEqualTo(strOffset + (str.Length + 1)));
Assert.That(StringHelper.GetString(((IntPtr)mem).Offset(strOffset), CharSet.Ansi), Is.EqualTo(str));
var tsout = (TestStrStructA)m.MarshalNativeToManaged(mem, mem.Size);
Assert.That(tsout.iVal, Is.EqualTo(ts.iVal));
Assert.That(tsout.array, Is.EquivalentTo(str));
}
m = new AnySizeStringMarshaler<TestStrStructA>(nameof(TestStrStructA.iVal) + ":br");
using (var mem = m.MarshalManagedToNative(ts))
{
var newStr = str.Substring(0, Math.Min(ts.iVal, str.Length));
Assert.That((long)mem.Size, Is.GreaterThanOrEqualTo(strOffset + ts.iVal));
Assert.That(StringHelper.GetString(((IntPtr)mem).Offset(strOffset), CharSet.Ansi), Is.EqualTo(newStr));
var tsout = (TestStrStructA)m.MarshalNativeToManaged(mem, mem.Size);
Assert.That(tsout.iVal, Is.EqualTo(ts.iVal));
Assert.That(tsout.array, Is.EquivalentTo(newStr));
}
}
const string str = "l;kajsdfl;kajsdl;fkj";
[Test]
public void StringNoFieldTest()
{
var ts = new TestStrStructU { iVal = str.Length + 1, array = str };
var strOffset = Marshal.OffsetOf<TestStrStructU>(nameof(TestStrStructU.array)).ToInt64();
IVanaraMarshaler m = new AnySizeStringMarshaler<TestStrStructU>("*");
using var mem = m.MarshalManagedToNative(ts);
Assert.That((long)mem.Size, Is.GreaterThanOrEqualTo(strOffset + (str.Length + 1) * StringHelper.GetCharSize(CharSet.Unicode)));
Assert.That(StringHelper.GetString(((IntPtr)mem).Offset(strOffset), CharSet.Unicode), Is.EqualTo(str));
var tsout = (TestStrStructU)m.MarshalNativeToManaged(mem, mem.Size);
Assert.That(tsout.iVal, Is.EqualTo(ts.iVal));
Assert.That(tsout.array, Is.EquivalentTo(str));
}
}
}