解决在升级 dotnet 9 之后 某些 P/Invoke 代码异常的问题

最近在使用第三方的某sdk的时候,发现示例程序一切正常,但是同样的代码在 .net9 上就直接闪退

1
2
3
[DllImport("SoftLic64.dll",CallingConvention=CallingConvention.StdCall,CharSet=CharSet.Ansi)]
public static extern string ks_cmd(string cmdType, string cmdData);

在 x64dbg 里面显示的是 STATUS_STACK_BUFFER_OVERRUN ,不过我看不懂汇编,也没法分析和正常的调用有什么差异

1
2
3
4
5
6
7
8
9
10
11
INT3 断点 "DllMain (softlic64.dll)" 于 <softlic64.OptionalHeader.AddressOfEntryPoint> (00007FFBC51474F0) !
EXCEPTION_DEBUG_INFO:
dwFirstChance: 0
ExceptionCode: C0000409 (STATUS_STACK_BUFFER_OVERRUN)
ExceptionFlags: 00000001
ExceptionAddress: kernelbase.00007FFD2E8D8711
NumberParameters: 3
ExceptionInformation[00]: 0000000000000039
ExceptionInformation[01]: 00000086AF8FEE30
ExceptionInformation[02]: 0000000040024000
第二次异常于 00007FFD2E8D8711 (C0000409, STATUS_STACK_BUFFER_OVERRUN)!

起初以为是 dll 只支持 framework, 但是在改为 net8.0 之后代码又能正常运行了。
但是去 dotnet 官网看了一圈也没有发现什么端倪,于是就想去 issue 里搜关键词 STATUS_STACK_BUFFER_OVERRUN 碰碰运气

结果还真发现了解决方法 NET 9.0 RC2 NativeLibrary.Load crashes #107993

原来 .net9 的 Interop 默认启用了 CET 支持: https://learn.microsoft.com/zh-cn/dotnet/core/compatibility/interop/9.0/cet-support

解决方法是在项目文件里面禁用 CET 即可。

1
<CETCompat>false</CETCompat>

在WPF上使用ViewLocator来自动定位View

在 Avalonia 中,ViewLocator 是一个用于解析与特定视图模型对应的视图(用户界面)的机制。
例如,给定一个名为 MyApplication.ViewModels.ExampleViewModel 的视图模型,视图定位器将查找一个名为 MyApplication.Views.ExampleView 的视图。
视图定位器通常与DataContext属性一起使用,该属性用于将视图与其视图模型关联起来。

而在WPF中,我们也可以通过继承ResourceDictionary,创建 DataTemplate 来实现这个功能。

创建 ViewLocator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ViewLocator: ResourceDictionary
{
public static ViewLocator Instance = new ViewLocator();

private ViewLocator()
{
}

public DataTemplate Locate<TVM,TView>()
{
return Locate(typeof(TVM),typeof(TView));
}

public DataTemplate Locate(Type t1,Type t2)
{
DataTemplate dt = new DataTemplate()
{
DataType = t1,
};
dt.VisualTree = new FrameworkElementFactory(t2);
try
{
this.Add(dt.DataTemplateKey, dt);

}
catch (System.ArgumentException)
{
//nothing
}
return dt;
}
}

使用 ViewLocator

关联 View 和 ViewModel

1
ViewLocator.Instance.Locate<FooViewModel,FooView>();

放置一个 ContentControl 到想要切换视图的区域,或者直接对 Window 使用。

1
<ContentControl x:Name="content_area" Resources="{x:Static local:ViewLocator.Instance}" />

切换视图

1
content_area.Content = new FooViewModel();

如果一切正常,此时 ContentControl 里显示的就是 FooView 控件的内容,而且 FooView 的 DataContext 也会自动关联 FooViewModel 。

使用 Source Generator 自动生成(反)序列化二进制数据代码

我们在遇到比较罕见/自定义的通信协议的时候,可能nuget上并没有相关的包来处理,需要自己写(反)序列化程序。
此时如果为每个类型的消息都单独解析就显得麻烦了。Source Generator 就能很好解决这个问题。

A Source Generator is a new kind of component that C# developers can write that lets you do two major things:

  1. Retrieve a compilation object that represents all user code that is being compiled. This object can be inspected, and you can write code that works with the syntax and semantic models for the code being compiled, just like with analyzers today.
  2. Generate C# source files that can be added to a compilation object during compilation. In other words, you can provide additional source code as input to a compilation while the code is being compiled.

单个消息的封送

SOME/IP(Scalable service-Oriented MiddlewarE over IP) 协议为例:
SOME/IP Header Format
我们可以很快用struct表示出它的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public partial struct SomeIpMessage
{

public Header Header;
public uint Length;
public RequestId RequestId;
public ProtocolInfo ProtocolInfo;

public byte[] PayLoad;
}
public partial struct ProtocolInfo
{
public byte ProtocolVersion;
public byte InterfaceVersion;
public byte MessageType;
public byte ReturnCode;

}
public partial struct RequestId
{
public UInt16 ClientId;
public UInt16 SessionId;
}
public partial struct Header
{
public UInt16 ServiceId;
public UInt16 MethodId;
}

这里也有一个示例数据(大端存储):

1
2
3
4
5
0000   50 03 80 01 00 00 00 45 00 00 00 00 01 01 02 00   P......E........
0010 00 00 00 39 00 00 00 00 00 00 00 00 00 00 00 00 ...9............
0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0040 00 00 00 00 00 00 00 00 00 00 00 00 00 .............

同时我们也可以很容易写出读写的方法 (以 Header 为例) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public partial struct Header
{
public UInt16 ServiceId;
public UInt16 MethodId;

public Header Deserialize(byte[] data)
{
var position = 0;

this.ServiceId = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(position));
position += sizeof(UInt16);
this.MethodId = BinaryPrimitives.ReadUInt16BigEndian(data.AsSpan(position));
position += sizeof(UInt16);

return this;
}

public byte[] Serialize()
{
var data = new byte[GetSize()];

var position = 0;
BinaryPrimitives.WriteUInt16BigEndian(data.AsSpan(position), this.ServiceId);
position += sizeof(UInt16);
BinaryPrimitives.WriteUInt16BigEndian(data.AsSpan(position), this.MethodId);
position += sizeof(UInt16);

return data;
}

public int GetSize()
{
return 0 + sizeof(UInt16) + sizeof(UInt16);
}
}


// Usage:
var sampleData = Convert.FromBase64String("UAOAAQAAAEUAAAAAAQECAAAAADkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
var rawHeader = sampleData.AsSpan(0,4).ToArray();

var header = new Header().Deserialize(rawHeader);
var newHeader = header.Serialize();

Enumerable.SequenceEqual(rawHeader,newHeader); // true

对于 byte[] 类型来说,它的长度可以由 (总大小-其它成员的大小)/sizeof(byte) 取得:

1
2
3
4
5
6
7
8
9
10
11
12
public byte[] ReadFromBytes(byte[] _, byte[] srcBytes, ref int startIndex, bool isBigEndian, object parent)
{
var arrayLength = (srcBytes.Length - startIndex) / sizeof(byte);
int byteCount = arrayLength * sizeof(byte);

var result = new byte[arrayLength];

Buffer.BlockCopy(srcBytes, startIndex, result, 0, byteCount);
startIndex += byteCount;

return result;
}

制作思路

为了自动生成封送代码,我们可以分成下面几个步骤:

  1. 寻找所有需要实现的 struct
  2. 遍历 struct 的每个成员
  3. 生成实现 (反)序列化 相关的代码

创建二进制处理的辅助项目

创建 .net standard 2.0 类库 StructPacker.Tools
定义封送的接口

1
2
3
4
5
6
7
8
9
10
11
public interface IPackable<out T>:ISizeInfo
{
T Deserialize(byte[] sourceData,bool isBigEndian=true, int startIndex = 0);

byte[] Serialize(bool isBigEndian=true);
}

public interface ISizeInfo
{
int GetSize();
}

定义相关 Attributes:
PackableAttribute 用来表示需要处理的 struct
PackIgnoreAttribute 用于忽略不需要处理的成员

1
2
3
4
5
6
7
8
9
10
11
[AttributeUsage(AttributeTargets.Struct)]
public class PackableAttribute:Attribute
{

}
[AttributeUsage(AttributeTargets.Field|AttributeTargets.Property)]
public class PackIgnoreAttribute : Attribute
{

}

添加二进制数据读方法

BinaryUtils.Readers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
public partial class BinaryUtils
{
#region private methods

private static short ReadInt16(byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
var value = isBigEndian
? BinaryPrimitives.ReadInt16BigEndian(srcBytes.AsSpan(startIndex))
: BinaryPrimitives.ReadInt16LittleEndian(srcBytes.AsSpan(startIndex));
startIndex += sizeof(Int16);
return value;
}

private static ushort ReadUInt16(byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
var value = isBigEndian
? BinaryPrimitives.ReadUInt16BigEndian(srcBytes.AsSpan(startIndex))
: BinaryPrimitives.ReadUInt16LittleEndian(srcBytes.AsSpan(startIndex));
startIndex += sizeof(Int16);
return value;
}

private static int ReadInt32(byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
var value = isBigEndian
? BinaryPrimitives.ReadInt32BigEndian(srcBytes.AsSpan(startIndex))
: BinaryPrimitives.ReadInt32LittleEndian(srcBytes.AsSpan(startIndex));
startIndex += sizeof(Int32);
return value;
}

private static uint ReadUInt32(byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
var value = isBigEndian
? BinaryPrimitives.ReadUInt32BigEndian(srcBytes.AsSpan(startIndex))
: BinaryPrimitives.ReadUInt32LittleEndian(srcBytes.AsSpan(startIndex));
startIndex += sizeof(UInt32);
return value;
}

private static Int64 ReadInt64(byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
var value = isBigEndian
? BinaryPrimitives.ReadInt64BigEndian(srcBytes.AsSpan(startIndex))
: BinaryPrimitives.ReadInt64LittleEndian(srcBytes.AsSpan(startIndex));
startIndex += sizeof(Int64);
return value;
}

private static UInt64 ReadUInt64(byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
var value = isBigEndian
? BinaryPrimitives.ReadUInt64BigEndian(srcBytes.AsSpan(startIndex))
: BinaryPrimitives.ReadUInt64LittleEndian(srcBytes.AsSpan(startIndex));
startIndex += sizeof(UInt64);
return value;
}


private static float ReadSingle(byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
unsafe
{
int val = ReadInt32(srcBytes, ref startIndex, isBigEndian);
return *(float*)&val;
}
}

private static double ReadDouble(byte[] value, ref int index, bool isBigEndian)
{
unsafe
{
long val = ReadInt64(value, ref index, isBigEndian);
return *(double*)&val;
}
}

private static decimal ReadDecimal(byte[] value, ref int index, bool isBigEndian)
{
if (isBigEndian)
{
throw new NotImplementedException();
}

unsafe
{
fixed (byte* pbyte = &value[index])
{
index += sizeof(decimal);
return *((decimal*)pbyte);
}
}
}

#endregion

public static T ReadFromBytes<T>(T _, byte[] srcBytes, ref int startIndex, bool isBigEndian)
where T : IPackable<T>, new()
{
var obj = new T();
obj.Deserialize(srcBytes, isBigEndian, startIndex);
startIndex += obj.GetSize();
return obj;
}

public static bool ReadFromBytes(bool _, byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
return srcBytes[startIndex++] switch
{
FlagTrue => true,
FlagFalse => false,
_ => throw new ArgumentOutOfRangeException()
};
}

public static byte ReadFromBytes(byte _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
srcBytes[startIndex++];

public static sbyte ReadFromBytes(sbyte _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
(sbyte)srcBytes[startIndex++];

public static char ReadFromBytes(char _, byte[] srcBytes, ref int startIndex, bool isBigEndian)
{
return (char)ReadInt16(srcBytes, ref startIndex, isBigEndian);
}

public static short ReadFromBytes(short _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadInt16(srcBytes, ref startIndex, isBigEndian);

public static ushort ReadFromBytes(ushort _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadUInt16(srcBytes, ref startIndex, isBigEndian);

public static int ReadFromBytes(int _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadInt32(srcBytes, ref startIndex, isBigEndian);

public static uint ReadFromBytes(uint _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadUInt32(srcBytes, ref startIndex, isBigEndian);

public static long ReadFromBytes(long _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadInt64(srcBytes, ref startIndex, isBigEndian);

public static ulong ReadFromBytes(ulong _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadUInt64(srcBytes, ref startIndex, isBigEndian);

public static float ReadFromBytes(float _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadSingle(srcBytes, ref startIndex, isBigEndian);

public static double ReadFromBytes(double _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadDouble(srcBytes, ref startIndex, isBigEndian);

public static decimal ReadFromBytes(decimal _, byte[] srcBytes, ref int startIndex, bool isBigEndian) =>
ReadDecimal(srcBytes, ref startIndex, isBigEndian);
}

添加二进制数据写方法

BinaryUtils.Writers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
public partial class BinaryUtils
{
#region private methods


private static void WriteInt16(short value, byte[] targetBytes, ref int index, bool isBigEndian)
{
if (isBigEndian)
BinaryPrimitives.WriteInt16BigEndian(targetBytes.AsSpan(index), value);
else
BinaryPrimitives.WriteInt16LittleEndian(targetBytes.AsSpan(index), value);

index += sizeof(short);
}

private static void WriteUInt16(ushort value, byte[] targetBytes, ref int index, bool isBigEndian)
{
if (isBigEndian)
BinaryPrimitives.WriteUInt16BigEndian(targetBytes.AsSpan(index), value);
else
BinaryPrimitives.WriteUInt16LittleEndian(targetBytes.AsSpan(index), value);

index += sizeof(ushort);
}

private static void WriteInt32(int value, byte[] targetBytes, ref int index, bool isBigEndian)
{
if (isBigEndian)
BinaryPrimitives.WriteInt32BigEndian(targetBytes.AsSpan(index), value);
else
BinaryPrimitives.WriteInt32LittleEndian(targetBytes.AsSpan(index), value);

index += sizeof(int);
}
private static void UWriteInt32(uint value, byte[] targetBytes, ref int index, bool isBigEndian)
{
if (isBigEndian)
BinaryPrimitives.WriteUInt32BigEndian(targetBytes.AsSpan(index), value);
else
BinaryPrimitives.WriteUInt32LittleEndian(targetBytes.AsSpan(index), value);

index += sizeof(uint);
}
private static void WriteInt64(long value, byte[] targetBytes, ref int index, bool isBigEndian)
{
if (isBigEndian)
BinaryPrimitives.WriteInt64BigEndian(targetBytes.AsSpan(index), value);
else
BinaryPrimitives.WriteInt64LittleEndian(targetBytes.AsSpan(index), value);

index += sizeof(long);
}
private static void UWriteInt64(ulong value, byte[] targetBytes, ref int index, bool isBigEndian)
{
if (isBigEndian)
BinaryPrimitives.WriteUInt64BigEndian(targetBytes.AsSpan(index), value);
else
BinaryPrimitives.WriteUInt64LittleEndian(targetBytes.AsSpan(index), value);

index += sizeof(ulong);
}

#endregion



public static void Write<T>(T obj, byte[] targetBytes, ref int index, bool isBigEndian) where T : IPackable<T>, new()
{
var data1 = obj.Serialize(isBigEndian);
data1.CopyTo(targetBytes, index);
index += data1.Length;
}

public static void Write<T>(T[] obj, byte[] targetBytes, ref int index, bool isBigEndian) where T : IPackable<T>, new()
{
foreach (var item in obj)
{
var data1 = item.Serialize(isBigEndian);
data1.CopyTo(targetBytes, index);
index += data1.Length;
}
}


public static void Write(bool value, byte[] targetBytes, ref int index, bool isBigEndian) =>
targetBytes[index++] = value ? FlagTrue : FlagFalse;

public static void Write(byte value, byte[] targetBytes, ref int index, bool isBigEndian) => targetBytes[index++] = value;
public static void Write(sbyte value, byte[] targetBytes, ref int index, bool isBigEndian) => targetBytes[index++] = (byte)value;

public static void Write(char value, byte[] targetBytes, ref int index, bool isBigEndian) =>
WriteInt16((short)value, targetBytes, ref index, isBigEndian);

public static void Write(short value, byte[] targetBytes, ref int index, bool isBigEndian) =>
WriteInt16(value, targetBytes, ref index, isBigEndian);

public static void Write(ushort value, byte[] targetBytes, ref int index, bool isBigEndian) =>
WriteInt16((short)value, targetBytes, ref index, isBigEndian);

public static void Write(int value, byte[] targetBytes, ref int index, bool isBigEndian) =>
WriteInt32(value, targetBytes, ref index, isBigEndian);

public static void Write(uint value, byte[] targetBytes, ref int index, bool isBigEndian) =>
WriteInt32((int)value, targetBytes, ref index, isBigEndian);

public static void Write(long value, byte[] targetBytes, ref int index, bool isBigEndian) =>
WriteInt64(value, targetBytes, ref index, isBigEndian);

public static void Write(ulong value, byte[] targetBytes, ref int index, bool isBigEndian) =>
WriteInt64((long)value, targetBytes, ref index, isBigEndian);

public static void Write(float value, byte[] targetBytes, ref int index, bool isBigEndian)
{
unsafe
{
WriteInt32(*(int*)&value, targetBytes, ref index, isBigEndian);
}
}

public static void Write(double value, byte[] targetBytes, ref int index, bool isBigEndian)
{
unsafe
{
WriteInt64(*(long*)&value, targetBytes, ref index, isBigEndian);
}
}

public static void Write(byte[] value, byte[] targetBytes,ref int index, bool isBigEndian)
{
int byteCount = value.Length * sizeof(byte);
Buffer.BlockCopy(value, 0, targetBytes, index, byteCount);
index += byteCount;
}
}

对于数组类型的数据,由于不同的协议中处理的方法不同,因此可以考虑单独开放自定义数组处理方法

1
2
3
4
5
6
7
8
9
[AttributeUsage(AttributeTargets.Field|AttributeTargets.Property)]
public class CustomBinaryConvertorAttribute : Attribute
{
public CustomBinaryConvertorAttribute(Type convertorType)
{
ConvertorType = convertorType;
}
public Type ConvertorType { get; }
}

定义自定义转换器的接口

1
2
3
4
5
6
public interface IArrayBinaryConvertor<T>
{
public T[] ReadFromBytes(T[] _, byte[] srcBytes, ref int startIndex, bool isBigEndian,object parent);

public void Write(T[] obj, byte[] targetBytes, ref int index, bool isBigEndian);
}

保存自定义转换器 :
BinaryUtils.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static readonly Encoding StringEncoding = Encoding.UTF8;

private static readonly Dictionary<Type, object> CustomConvertors = new();
public static T GetConvertor<T>() where T: class
{
if (CustomConvertors.ContainsKey(typeof(T)))
{
return CustomConvertors[typeof(T)] as T;
}
else
{
var instance = Activator.CreateInstance<T>();
CustomConvertors.Add(typeof(T),instance);
return instance;
}
}

下面我们就可以尝试手动实现一下
假设当前有一个测试结构体 StructA

1
2
3
4
5
6
public partial struct StructA
{
public int Id;
[CustomBinaryConvertor(typeof(AutoSizeArrayConvertor))]
public byte[] Data;
}

实现 IPackable<StructA> 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public partial struct StructA: IPackable<StructA>
{
public int GetSize()=>0 + BinaryUtils.GetSize(Id) + BinaryUtils.GetSize(Data);

public byte[] Serialize(bool isBigEndian=true)
{
var destBytes = new byte[GetSize()];
int startIndex = 0;
// 默认
BinaryUtils.Write(Id, destBytes, ref startIndex, isBigEndian);

// 自定义转换器
BinaryUtils.GetConvertor<AutoSizeArrayConvertor>().Write(Data, destBytes, ref startIndex, isBigEndian);

return destBytes;
}
public StructA Deserialize(byte[] sourceData, bool isBigEndian=true, int startIndex = 0)
{
// 默认
Id = BinaryUtils.ReadFromBytes(Id, sourceData, ref startIndex, isBigEndian);

// 自定义转换器
Data = BinaryUtils.GetConvertor<StructPacker.Tests.AutoSizeArrayConvertor>().ReadFromBytes(Data, sourceData, ref startIndex, isBigEndian, this);

return this;
}
}

定义处理自动长度的数组的转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AutoSizeArrayConvertor:IArrayBinaryConvertor<byte>
{
public byte[] ReadFromBytes(byte[] _, byte[] srcBytes, ref int startIndex, bool isBigEndian, object parent)
{
var arrayLength = (srcBytes.Length - startIndex) / sizeof(byte);
int byteCount = arrayLength * sizeof(byte);

var result = new byte[arrayLength];

Buffer.BlockCopy(srcBytes, startIndex, result, 0, byteCount);
startIndex += byteCount;

return result;

}

public void Write(byte[] obj, byte[] targetBytes, ref int index, bool isBigEndian)
{
BinaryUtils.Write(obj, targetBytes, ref index, isBigEndian);
}
}

之后就可以尝试编写 Source Generator 了!

创建Source Generator项目

创建 .net standard 2.0 类库 StructPacker 并且添加nuget包:

1
2
3
4
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>

实现 ISyntaxReceiver

The ISyntaxReceiver can record any information about the nodes visited. During Execute(GeneratorExecutionContext) the generator can obtain the created instance via the SyntaxReceiver property. The information contained can be used to perform final generation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class StructReceiver:ISyntaxReceiver
{
public List<StructDeclarationSyntax> PackableStructItems { get; } = new();

public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is StructDeclarationSyntax decSyntax)
{
if (decSyntax.AttributeLists.Any(x=>x.Attributes.Any(y=> y.ToFullString()== "Packable")))
{
PackableStructItems.Add(decSyntax);
}
}
}
}

实现 ISourceGenerator

StructPackerGenerator.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
[Generator]
public class StructPackerGenerator : ISourceGenerator
{
private const string AttributeNamespace = "StructPacker.Tools.Attributes";

public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new StructReceiver());
}

public void Execute(GeneratorExecutionContext context)
{
var receiver = (StructReceiver)context.SyntaxReceiver;

if (receiver == null || receiver.PackableStructItems.Count == 0)
return;
try
{
GenerateSourceFile(receiver.PackableStructItems, context);
}
catch (Exception e)
{
var diag = Diagnostic.Create("RKSPE", "StructPacker", $"StructPacker: {e.Message}",
DiagnosticSeverity.Error, DiagnosticSeverity.Error, true, 0);
context.ReportDiagnostic(diag);
}
}

private void GenerateSourceFile(List<StructDeclarationSyntax> inputStructs, GeneratorExecutionContext context)
{
foreach (StructDeclarationSyntax declaration in inputStructs)
{
string code = GenerateCode(declaration, context);
if (string.IsNullOrEmpty(code))
{
continue;
}
context.AddSource($"{GetNamespace(declaration)}.{declaration.Identifier.Text}.g.cs", code);
}
}

private string GenerateCode(TypeDeclarationSyntax declaration, GeneratorExecutionContext context)
{
var code = new CodeGen();
code.AppendUsings("StructPacker.Tools");
code.AppendUsings("StructPacker.Tools.Utils");
code.Line();
code.BeginNamespace(GetNamespace(declaration));
SemanticModel model = context.Compilation.GetSemanticModel(declaration.SyntaxTree);

ISymbol decSymb = ModelExtensions.GetDeclaredSymbol(model, declaration);
string classFqn = decSymb?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
if (string.IsNullOrWhiteSpace(classFqn))
return null;

var fields = new List<StructFieldInfo>();

foreach (PropertyDeclarationSyntax p in declaration.Members.OfType<PropertyDeclarationSyntax>())
{
if (HasAttribute(p.AttributeLists, $"{AttributeNamespace}.PackIgnoreAttribute", model, context))
continue;

if (p.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)))
continue;

AccessorDeclarationSyntax getter =
p.AccessorList?.Accessors.FirstOrDefault(x => x.IsKind(SyntaxKind.GetAccessorDeclaration));
AccessorDeclarationSyntax setter =
p.AccessorList?.Accessors.FirstOrDefault(x => x.IsKind(SyntaxKind.SetAccessorDeclaration));

if (getter == null || setter == null)
throw new Exception(
$"Type \"{classFqn}\" has properties with missing \"get\" or \"set\" accessors and cannot be serialized. Add missing accessors or skip serializing this property with \"SkipPack\" attribute if this is intentional.");

if (getter.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)) ||
setter.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)))
throw new Exception(
$"Type \"{classFqn}\" has properties with \"get\" or \"set\" accessors marked as private and cannot be serialized. Remove private modifier or skip serializing this property with \"SkipPack\" attribute if this is intentional.");

fields.Add(new StructFieldInfo(p.Identifier.Text,GetConverterType(p,model)));
}

foreach (FieldDeclarationSyntax p in declaration.Members.OfType<FieldDeclarationSyntax>())
{
if (HasAttribute(p.AttributeLists, $"{AttributeNamespace}.PackIgnoreAttribute", model, context))
continue;

if (p.Modifiers.Any(m => m.IsKind(SyntaxKind.PrivateKeyword)))
continue;

foreach (VariableDeclaratorSyntax varSyn in p.Declaration.Variables)
fields.Add(new StructFieldInfo(varSyn.Identifier.Text,GetConverterType(p,model)));
}

Generate(declaration.Identifier.Text, fields, code);
return code.ToString();
}

private void Generate(string identifier, List<StructFieldInfo> fields, CodeGen code)
{

if (fields.Count == 0)
throw new Exception(
$"Type \"{identifier}\" does not contain any valid members. Serializing empty types is meaningless as they take zero bytes. Add some members or exclude this type from serialization.");

code.Line($"public partial struct {identifier}:IPackable<{identifier}>");
code.BeginCodeBlock();

// Implement GetSize
code.Line($"public int GetSize()=>0");
foreach (var id in fields)
{
code.Append($" + {FqnTools}.GetSize({id.StructName})");
}
code.Append(";");
code.Line("");
// Implement Serialize
code.Line($"public byte[] Serialize(bool isBigEndian=true)");
code.BeginCodeBlock();
code.Line("var destBytes = new byte[GetSize()];");
code.Line("int startIndex = 0;");

foreach (var id in fields)
{
if (id.Convertor!=null)
{
code.Line($"{FqnTools}.GetConvertor<{id.Convertor}>().Write({id.StructName}, destBytes, ref startIndex, isBigEndian);");
}
else
{
code.Line($"{FqnTools}.Write({id.StructName}, destBytes, ref startIndex, isBigEndian);");
}
}

code.Line("return destBytes;");
code.EndCodeBlock();

// Implement Deserialize
code.Line($"public {identifier} Deserialize(byte[] sourceData, bool isBigEndian=true, int startIndex = 0)");
code.BeginCodeBlock();

foreach (var id in fields)
{
if (id.Convertor!=null)
{
code.Line($"{id.StructName} = {FqnTools}.GetConvertor<{id.Convertor}>().ReadFromBytes({id.StructName}, sourceData, ref startIndex, isBigEndian, this);");

}
else
{
code.Line(
$"{id.StructName} = {FqnTools}.ReadFromBytes({id.StructName}, sourceData, ref startIndex, isBigEndian);");
}
}

code.Line($"return this;");

code.EndCodeBlock();


//end


code.EndCodeBlock();
code.EndCodeBlock();



}

private const string FqnTools = "BinaryUtils";


#region SyntaxUtils

// Custom converter
private ITypeSymbol? GetConverterType(PropertyDeclarationSyntax field,SemanticModel semanticModel)
{
var attributes = field.AttributeLists;
foreach (var attribute in attributes)
{
foreach (var attributeAttribute in attribute.Attributes)
{
if (attributeAttribute.Name.ToFullString() == "CustomBinaryConvertor")
{
AttributeArgumentSyntax firstArgument = attributeAttribute.ArgumentList.Arguments.First();

var typeExpressionSyntax = firstArgument.Expression as TypeOfExpressionSyntax;
if (typeExpressionSyntax == null)
{
return null;
}

// Parse convertor type from the syntax tree
var typeInfo = semanticModel.GetTypeInfo(typeExpressionSyntax.Type);

return typeInfo.Type;

}
}
}

return null;
}

private ITypeSymbol? GetConverterType(FieldDeclarationSyntax field,SemanticModel semanticModel)
{
var attributes = field.AttributeLists;
foreach (var attribute in attributes)
{
foreach (var attributeAttribute in attribute.Attributes)
{
if (attributeAttribute.Name.ToFullString() == "CustomBinaryConvertor")
{
AttributeArgumentSyntax firstArgument = attributeAttribute.ArgumentList.Arguments.First();

// var value = firstArgument.Expression.NormalizeWhitespace().ToFullString();
var typeExpressionSyntax = firstArgument.Expression as TypeOfExpressionSyntax;
if (typeExpressionSyntax == null)
{
return null;
}

// Parse convertor type from the syntax tree
var typeInfo = semanticModel.GetTypeInfo(typeExpressionSyntax.Type);

return typeInfo.Type;

}
}
}

return null;
}



internal static readonly SymbolDisplayFormat TypeNameFormat = new(
SymbolDisplayGlobalNamespaceStyle.Omitted,
SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);

internal static bool HasAttribute(SyntaxList<AttributeListSyntax> source, string fullName, SemanticModel model,
GeneratorExecutionContext ctx)
{
return source.SelectMany(list => list.Attributes).Any(
atr =>
{
TypeInfo typeInfo = ModelExtensions.GetTypeInfo(model, atr, ctx.CancellationToken);
string typeName = typeInfo.Type?.ToDisplayString(TypeNameFormat);

return string.Equals(typeName, fullName, StringComparison.Ordinal);
});
}

// determine the namespace the class/enum/struct is declared in, if any
// refer: https://andrewlock.net/creating-a-source-generator-part-5-finding-a-type-declarations-namespace-and-type-hierarchy/
static string GetNamespace(BaseTypeDeclarationSyntax syntax)
{
// If we don't have a namespace at all we'll return an empty string
// This accounts for the "default namespace" case
string nameSpace = string.Empty;

// Get the containing syntax node for the type declaration
// (could be a nested type, for example)
SyntaxNode? potentialNamespaceParent = syntax.Parent;

// Keep moving "out" of nested classes etc until we get to a namespace
// or until we run out of parents
while (potentialNamespaceParent != null &&
potentialNamespaceParent is not NamespaceDeclarationSyntax
&& potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax)
{
potentialNamespaceParent = potentialNamespaceParent.Parent;
}

// Build up the final namespace by looping until we no longer have a namespace declaration
if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent)
{
// We have a namespace. Use that as the type
nameSpace = namespaceParent.Name.ToString();

// Keep moving "out" of the namespace declarations until we
// run out of nested namespace declarations
while (true)
{
if (namespaceParent.Parent is not NamespaceDeclarationSyntax parent)
{
break;
}

// Add the outer namespace as a prefix to the final namespace
nameSpace = $"{namespaceParent.Name}.{nameSpace}";
namespaceParent = parent;
}
}

// return the final namespace
return nameSpace;
}

// refer: https://github.com/dotnet/roslyn/issues/28233
// not working for this project :(
static string GetNamespace(BaseTypeDeclarationSyntax syntax, GeneratorExecutionContext ctx)
{
SemanticModel semanticModel = ctx.Compilation.GetSemanticModel(syntax.SyntaxTree);
var typeSymbol = semanticModel.GetSymbolInfo(syntax).Symbol as INamedTypeSymbol;
if (typeSymbol is INamedTypeSymbol symbol)
{
return symbol.ContainingNamespace.Name;
}

throw new Exception($"Could not find namespace for type {syntax}");
}

#endregion
}

使用 SourceGenerator

创建单元测试项目 StructPacker.Tests

引用 刚写好的 StructPacker:

1
2
3
4
<ItemGroup>
<ProjectReference Include="..\StructPacker.Tools\StructPacker.Tools.csproj" />
<ProjectReference Include="..\StructPacker\StructPacker.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

定义 SomeIpMessage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[Packable]
public partial struct SomeIpMessage
{

public Header Header;
public uint Length;
public RequestId RequestId;
public ProtocolInfo ProtocolInfo;

[CustomBinaryConvertor(typeof(AutoSizeArrayConvertor))]
public byte[] PayLoad;
}
[Packable]
public partial struct ProtocolInfo
{
public byte ProtocolVersion;
public byte InterfaceVersion;
public byte MessageType;
public byte ReturnCode;

}
[Packable]
public partial struct RequestId
{
public UInt16 ClientId;
public UInt16 SessionId;
}
[Packable]
public partial struct Header
{
public UInt16 ServiceId;
public UInt16 MethodId;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

[TestMethod]
public void TestSomeIpMessage()
{
var sampleMsg = Convert.FromBase64String("UAOAAQAAAEUAAAAAAQECAAAAADkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");

var msgObj = new SomeIpMessage().Deserialize(sampleMsg);
var newMsg = msgObj.Serialize();

var ret = sampleMsg.SequenceEqual(newMsg);
Assert.IsTrue(ret,"Compare raw and new data."); // true
}

恭喜,现在它已经可以自动实现相关的方法了!!

参考链接

source-generators-overview
RudolfKurkaMs/StructPacker
C# Source Generators: Getting Started
Get attribute arguments with roslyn
Finding a type declaration’s namespace and type hierarchy

为 WPF 的 ScrollViewer 添加滚动动画

在 WPF 中,ScrollViewer 默认没有直接支持滚动动画的属性。不过,我们可以通过自定义附加属性和动画来实现滚动动画效果。

定义附加属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

public static class ScrollViewerBehavior
{
public static readonly DependencyProperty HorizontalOffsetProperty =
DependencyProperty.RegisterAttached("HorizontalOffset", typeof(double), typeof(ScrollViewerBehavior),
new UIPropertyMetadata(0.0, OnHorizontalOffsetChanged));

public static void SetHorizontalOffset(FrameworkElement target, double value) =>
target.SetValue(HorizontalOffsetProperty, value);

public static double GetHorizontalOffset(FrameworkElement target) =>
(double)target.GetValue(HorizontalOffsetProperty);

private static void OnHorizontalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) =>
(target as ScrollViewer)?.ScrollToHorizontalOffset((double)e.NewValue);

public static readonly DependencyProperty VerticalOffsetProperty =
DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), typeof(ScrollViewerBehavior),
new UIPropertyMetadata(0.0, OnVerticalOffsetChanged));

public static void VerticalOffset(FrameworkElement target, double value) =>
target.SetValue(VerticalOffsetProperty, value);

public static double GetVerticalOffset(FrameworkElement target) =>
(double)target.GetValue(VerticalOffsetProperty);

private static void OnVerticalOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e) =>
(target as ScrollViewer)?.ScrollToVerticalOffset((double)e.NewValue);

}

设置动画:

1
2
3
4
5
6
7
8
9
10
<Storyboard x:Key="ScrollStoryboard">
<DoubleAnimation Storyboard.TargetName="ScrollViewer"
Storyboard.TargetProperty="(YourNamespace:ScrollViewerBehavior.HorizontalOffset)"
From="0" To="500" Duration="0:0:0.6">
<DoubleAnimation.EasingFunction>
<CircleEase EasingMode="EaseOut"/>
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>