diff --git a/PInvoke/Shared/InteropServices/SafeAnysizeStruct.cs b/PInvoke/Shared/InteropServices/SafeAnysizeStruct.cs index 149475d8..31b9efe0 100644 --- a/PInvoke/Shared/InteropServices/SafeAnysizeStruct.cs +++ b/PInvoke/Shared/InteropServices/SafeAnysizeStruct.cs @@ -20,7 +20,7 @@ namespace Vanara.InteropServices /// The initial value of the structure, if provided. /// /// The name of the field in that holds the length of the array. If , 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. /// /// This class can only manange sequential layout structures. public SafeAnysizeStruct(in T value, string sizeFieldName = null) : base(baseSz) @@ -34,7 +34,7 @@ namespace Vanara.InteropServices /// The size of the allocated memory in in bytes. /// /// The name of the field in that holds the length of the array. If , 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. /// 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 /// The type of the structure. public abstract class SafeAnysizeStructBase : SafeMemoryHandle { + internal const BindingFlags binds = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + /// The base, unextended size of T. 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; } } + + /// + /// A marshaler implementation of 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. + /// + /// Use the cookie paramter of to specify the name of the field in that + /// specifies the length of the string in the last field of along with use indicators. + /// + /// + /// 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). + /// + /// + /// 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). + /// + /// If the field name is "*", then the string length will be determined by the amount of allocated memory. + /// + /// The structure type to be marshaled. + public class AnySizeStringMarshaler : 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.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; + } + + /// Initializes a new instance of the class. + /// + /// The name of the field in that specifies the number of elements in the last field of . + /// + /// 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). + /// + /// + /// 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). + /// + /// + public AnySizeStringMarshaler(string cookie) + { + if (string.IsNullOrEmpty(cookie)) + fiCount = typeof(T).GetOrderedFields(SafeAnysizeStructBase.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.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; + } + } } \ No newline at end of file diff --git a/UnitTests/PInvoke/Shared/InteropServices/AnysizeStructTests.cs b/UnitTests/PInvoke/Shared/InteropServices/AnysizeStructTests.cs index f1443626..c3f34c0b 100644 --- a/UnitTests/PInvoke/Shared/InteropServices/AnysizeStructTests.cs +++ b/UnitTests/PInvoke/Shared/InteropServices/AnysizeStructTests.cs @@ -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(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(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(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(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(nameof(TestStrStructU.array)).ToInt64(); + + IVanaraMarshaler m = new AnySizeStringMarshaler(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(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(nameof(TestStrStructA.array)).ToInt64(); + + IVanaraMarshaler m = new AnySizeStringMarshaler(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(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(nameof(TestStrStructU.array)).ToInt64(); + + IVanaraMarshaler m = new AnySizeStringMarshaler("*"); + 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)); + } } } \ No newline at end of file