Skip to content

Commit 1e73252

Browse files
chore(v0.2): Improve RPC handling
Add IRpcMethodMarker to flag interfaces to be used for RPC, which makes it easier to extend with new services in future.
1 parent 46b56c3 commit 1e73252

File tree

14 files changed

+152
-44
lines changed

14 files changed

+152
-44
lines changed

.Zeugwerk/config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"name": "TcHaxx.Snappy",
77
"plcs": [
88
{
9-
"version": "0.1.0.0",
9+
"version": "0.2.0.0",
1010
"name": "snappy",
1111
"type": "Library",
1212
"packages": [

.editorconfig

+2-1
Original file line numberDiff line numberDiff line change
@@ -197,4 +197,5 @@ csharp_style_prefer_extended_property_pattern = true:suggestion
197197

198198
# SonarAnalyzer
199199
dotnet_diagnostic.S1134.severity = suggestion
200-
dotnet_diagnostic.S1135.severity = suggestion
200+
dotnet_diagnostic.S1135.severity = suggestion
201+
dotnet_diagnostic.S6602.severity = suggestion

README.md

+5-4
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,15 @@ Activate Configuration | Execute | `cmd /c start TcHaxx.Snappy.CLI verify -d \"%
119119
## Source control: Received and Verified files
120120
When dealing with source control, consider the following guidelines for handling **Received** and **Verified** files:
121121

122-
1. **Exclusion**:
122+
1. **Exclude files**:
123123
- Exclude all files with the pattern `*.received.*` from source control.
124124
- To achieve this, add the following line to your `.gitignore` file:
125125
```
126126
*.received.*
127127
```
128128
129-
2. **Commitment**:
130-
- On the other hand, **commit** all files with the pattern `*.verified.*` to source control.
129+
2. **Commit files**:
130+
- **Commit** all files with the pattern `*.verified.*` to source control.
131131
132132
> See [Verify/README](https://github.com/VerifyTests/Verify?tab=readme-ov-file#source-control-received-and-verified-files)
133133
@@ -169,4 +169,5 @@ Option | Required | Default | Description
169169
* [TcUnit](https://github.com/tcunit/TcUnit) - A unit testing framework for Beckhoff's TwinCAT 3
170170
* [CommandLineParser](https://github.com/commandlineparser/commandline) - A command line parsing library for .NET applications.
171171
* [Verify](https://github.com/VerifyTests/Verify) - A library used for snapshot testing.
172-
* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications.
172+
* [Serilog](https://github.com/serilog/serilog) - A logging library for .NET applications.
173+
* [TF6000_ADS_DOTNET_V5_Samples](https://github.com/Beckhoff/TF6000_ADS_DOTNET_V5_Samples) - Sample code for the Version 6.X series of the TwinCAT ADS .NET Packages

src/TcHaxx.Snappy.CLI/TcHaxx.Snappy.CLI.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<Description>A Snapshot Testing framework for TwinCAT 3</Description>
1111
<Copyright>Copyright (c) 2024 densogiaichned</Copyright>
1212
<Title>TwinCAT Snapshot Testing framework</Title>
13-
<Version>0.1.0</Version>
13+
<Version>0.2.0</Version>
1414
<AssemblyVersion>$(Version)</AssemblyVersion>
1515
<FileVersion>$(Version)</FileVersion>
1616
<PackAsTool>True</PackAsTool>
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using TcHaxx.Snappy.Common.Verify;
2-
3-
namespace TcHaxx.Snappy.Common.RPC;
1+
namespace TcHaxx.Snappy.Common.RPC;
42

53
/// <summary>
64
/// Describes an descriptor for RPC methods.
@@ -14,8 +12,8 @@ public interface IRpcMethodDescriptor
1412
public IEnumerable<RpcMethodDescription> GetRpcMethodDescription();
1513

1614
/// <summary>
17-
/// Registers a <see cref="IVerifyMethod"/> implementation with <see cref="IRpcMethodDescriptor"/>.
15+
/// Registers a <see cref="IRpcMethodMarker"/> implementation with <see cref="IRpcMethodDescriptor"/>.
1816
/// </summary>
19-
/// <param name="rpcVerifyMethod"></param>
20-
public void Register(IVerifyMethod rpcVerifyMethod);
17+
/// <param name="rpcMethod"></param>
18+
public void Register(IRpcMethodMarker rpcMethod);
2119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace TcHaxx.Snappy.Common.RPC;
2+
3+
/// <summary>
4+
/// Empty inteface to mark RPC methods.
5+
/// </summary>
6+
public interface IRpcMethodMarker
7+
{
8+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Reflection;
2-
using TcHaxx.Snappy.Common.Verify;
32

43
namespace TcHaxx.Snappy.Common.RPC;
54

6-
public record RpcMethodDescription(MethodInfo Method, IEnumerable<ParameterInfo> Parameters, ParameterInfo ReturnValue, IVerifyMethod RpcInvocableMethod);
5+
public record RpcMethodDescription(MethodInfo Method, IEnumerable<ParameterInfo> Parameters, ParameterInfo ReturnValue, IRpcMethodMarker RpcInvocableMethod, string? Alias)
6+
{
7+
public string InstanceName => RpcInvocableMethod.GetType().FullName!;
8+
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
using TcHaxx.Snappy.Common.Verify;
1+
using System.Data;
2+
using System.Reflection;
3+
using TcHaxx.Snappy.Common.RPC.Attributes;
24

35
namespace TcHaxx.Snappy.Common.RPC;
46

57
public class RpcMethodDescriptor : IRpcMethodDescriptor
68
{
7-
private readonly Queue<IVerifyMethod> _verifyMethods = new();
9+
private readonly Queue<IRpcMethodMarker> _verifyMethods = new();
810

911
public RpcMethodDescriptor()
1012
{
@@ -18,20 +20,57 @@ public IEnumerable<RpcMethodDescription> GetRpcMethodDescription()
1820
}
1921
}
2022

21-
public void Register(IVerifyMethod rpcVerifyMethod)
23+
public void Register(IRpcMethodMarker rpcMethod)
2224
{
23-
_verifyMethods.Enqueue(rpcVerifyMethod);
25+
_verifyMethods.Enqueue(rpcMethod);
2426
}
25-
26-
private RpcMethodDescription Transform(IVerifyMethod rpcVerifyMethod)
27+
private static RpcMethodDescription Transform(IRpcMethodMarker rpcMethod)
2728
{
28-
var method = rpcVerifyMethod.GetType().GetMethod(nameof(IVerifyMethod.Verify)) ??
29-
throw new RpcMethodTransformException($"Method \"{nameof(IVerifyMethod.Verify)}\" not found.");
29+
var method = GetMethodInfo(rpcMethod);
30+
3031
var parameters = method.GetParameters() ??
31-
throw new RpcMethodTransformException($"Expected method \"{nameof(IVerifyMethod.Verify)}\" to have parameters");
32+
throw new RpcMethodTransformException($"Expected method \"{method.Name}\" to have parameters");
3233

3334
var retVal = method.ReturnParameter;
3435

35-
return new RpcMethodDescription(method, parameters, retVal, rpcVerifyMethod);
36+
var alias = GetAliasAttriubte(rpcMethod);
37+
return new RpcMethodDescription(method, parameters, retVal, rpcMethod, alias);
38+
}
39+
40+
private static MethodInfo GetMethodInfo(IRpcMethodMarker rpcMethod)
41+
{
42+
var typeName = rpcMethod.GetType().Name;
43+
44+
var methodInfos = GetMethodInfos(rpcMethod);
45+
46+
if (methodInfos is null || methodInfos.Length == 0)
47+
{
48+
throw new RpcMethodTransformException($"No RPC method found in type \"{typeName}\".");
49+
}
50+
51+
if (methodInfos.Length > 1)
52+
{
53+
throw new RpcMethodTransformException($"Only one RPC method supported per type ({typeName}).");
54+
}
55+
56+
return methodInfos[0];
57+
}
58+
59+
private static string? GetAliasAttriubte(IRpcMethodMarker rpcMethod)
60+
{
61+
var aliasAttribute = rpcMethod.GetType()
62+
.GetMethods()
63+
.Select(x => x.GetCustomAttribute<AliasAttribute>())
64+
.FirstOrDefault(x => x is not null);
65+
return aliasAttribute?.AliasName;
66+
}
67+
68+
private static MethodInfo[]? GetMethodInfos(IRpcMethodMarker rpcMethod)
69+
{
70+
var interfaces = rpcMethod.GetType().GetInterfaces();
71+
var rpcInterfaces = interfaces
72+
.FirstOrDefault(i => typeof(IRpcMethodMarker).IsAssignableFrom(i) && i.GetInterfaces().Length == 1 && i.GetInterfaces().Contains(typeof(IRpcMethodMarker)));
73+
74+
return rpcInterfaces?.GetMethods();
3675
}
3776
}

src/TcHaxx.Snappy.Common/Verify/IVerifyMethod.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
using TcHaxx.Snappy.Common.RPC.Attributes;
1+
using TcHaxx.Snappy.Common.RPC;
2+
using TcHaxx.Snappy.Common.RPC.Attributes;
23

34
namespace TcHaxx.Snappy.Common.Verify;
45

5-
public interface IVerifyMethod
6+
public interface IVerifyMethod : IRpcMethodMarker
67
{
78
public VerificationResult Verify(
89
[String(Constants.DEFAULT_TEST_NAMES_LENGTH)] string testSuiteName,

src/TcHaxx.Snappy.TcADS/SymbolicServer.cs

+32-1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,36 @@ protected override void OnConnected()
6060
}
6161

6262
protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, IRpcMethod method, object[] values, out object? returnValue)
63+
{
64+
returnValue = null;
65+
var retVal = OnRpcInvokeProxy(structInstance, method, values, out returnValue);
66+
67+
if (retVal != AdsErrorCode.NoError)
68+
{
69+
// Note:
70+
// Due to a bug (?) in ADS.Net, all parameter values and the returnValue must not be NULL!
71+
// Otherwise, it will throw an exception during marshalling, hence the client will only receive error 0x1
72+
returnValue = 0;
73+
PresetParameterValues(ref values);
74+
}
75+
76+
return retVal;
77+
}
78+
79+
private static void PresetParameterValues(ref object[] parameterValues)
80+
{
81+
for (var i = 0; i < parameterValues.Length; i++)
82+
{
83+
if (parameterValues[i] is not null)
84+
{
85+
continue;
86+
}
87+
88+
parameterValues[i] = 0;
89+
}
90+
}
91+
92+
private AdsErrorCode OnRpcInvokeProxy(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue)
6393
{
6494
var iDataType = structInstance.DataType;
6595
if (iDataType is null)
@@ -68,7 +98,8 @@ protected override AdsErrorCode OnRpcInvoke(IInterfaceInstance structInstance, I
6898
_logger?.LogError("{OnRpcInvoke}: {IDataType} is null", nameof(OnRpcInvoke), nameof(IDataType));
6999
return AdsErrorCode.DeviceInvalidContext;
70100
}
101+
71102
_logger?.LogInformation("{OnRpcInvoke}: Invoking method {IRpcMethod} of {IDataTypeFullName}", nameof(OnRpcInvoke), method, iDataType.FullName);
72-
return _symbolFactory.InvokeRpcMethod(iDataType, values, out returnValue);
103+
return _symbolFactory.InvokeRpcMethod(structInstance, method, parameterValues, out returnValue);
73104
}
74105
}

src/TcHaxx.Snappy.TcADS/Symbols/ISymbolFactory.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ namespace TcHaxx.Snappy.TcADS.Symbols;
77
internal interface ISymbolFactory
88
{
99
void AddSymbols(ServerSymbolFactory? serverSymbolFactory);
10-
AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue);
10+
AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue);
1111
}

src/TcHaxx.Snappy.TcADS/Symbols/SymbolFactory.cs

+23-14
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Microsoft.Extensions.Logging;
55
using TcHaxx.Snappy.Common.RPC;
66
using TcHaxx.Snappy.Common.RPC.Attributes;
7-
using TcHaxx.Snappy.Common.Verify;
87
using TwinCAT.Ads;
98
using TwinCAT.Ads.Server.TypeSystem;
109
using TwinCAT.Ads.TypeSystem;
@@ -17,7 +16,7 @@ internal class SymbolFactory : ISymbolFactory
1716
private readonly IRpcMethodDescriptor _rpcMethodDescriptor;
1817
private readonly ILogger? _logger;
1918

20-
private readonly Dictionary<IDataType, IVerifyMethod> _mappedStructTypeToRpcMethod = [];
19+
private readonly Dictionary<IDataType, IRpcMethodMarker> _mappedStructTypeToRpcMethod = [];
2120

2221
internal SymbolFactory(IRpcMethodDescriptor rpcMethodDescriptor, ILogger? logger)
2322
{
@@ -42,47 +41,57 @@ public void AddSymbols(ServerSymbolFactory? serverSymbolFactory)
4241
var retValKvp = GetMethodReturnValue(rpcMethodDescription.ReturnValue);
4342
AddToServerSymbolFactory(serverSymbolFactory, [retValKvp]);
4443

45-
var fullName = rpcMethodDescription.Method.ReflectedType?.FullName ?? rpcMethodDescription.Method.Name;
44+
var fullName = rpcMethodDescription.InstanceName;
4645
var dataArea = new DataArea($"DATA::{fullName}", idxGrp, idxOffset++, 0x10000);
4746

4847
_ = serverSymbolFactory.AddDataArea(dataArea);
4948

50-
var rpc = BuildRpcMethod(rpcMethodDescription.Method, paramsKvp, retValKvp);
49+
var rpc = BuildRpcMethod(rpcMethodDescription, paramsKvp, retValKvp);
5150

5251
var dtStructRpc = new StructType($"STRUCT::{fullName}");
5352
_ = dtStructRpc.AddMethod(rpc);
5453
_ = serverSymbolFactory.AddType(dtStructRpc);
5554
_ = serverSymbolFactory.AddSymbol(fullName, dtStructRpc, dataArea);
5655

56+
_logger?.LogInformation("Adding RPC method {MappedTypeFullName}#{MethodName}", fullName, rpc.Name);
5757
_mappedStructTypeToRpcMethod.Add(dtStructRpc, rpcMethodDescription.RpcInvocableMethod);
5858
}
5959
}
6060

61-
public AdsErrorCode InvokeRpcMethod(IDataType mappedType, object[] values, out object? returnValue)
61+
public AdsErrorCode InvokeRpcMethod(IInterfaceInstance structInstance, IRpcMethod method, object[] parameterValues, out object? returnValue)
6262
{
63-
returnValue = null;
64-
if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var value))
63+
returnValue = Activator.CreateInstance(Type.GetType(method.ReturnType, false) ?? typeof(int));
64+
var mappedType = structInstance.DataType!;
65+
66+
if (!_mappedStructTypeToRpcMethod.TryGetValue(mappedType, out var rpcMethodType))
6567
{
68+
_logger?.LogError("No matching type found ({MappedTypeFullName})", mappedType.FullName);
6669
return AdsErrorCode.DeviceServiceNotSupported;
6770
}
6871

69-
var rpcMethodToInvoke = value;
72+
var rpcMethodToInvoke = rpcMethodType.GetType()
73+
.GetMethods()
74+
.FirstOrDefault(x => string.Equals(x.Name, method.Name, StringComparison.OrdinalIgnoreCase));
75+
if (rpcMethodToInvoke is null)
76+
{
77+
_logger?.LogError("Method \"{MethodName}\" not found in type \"{MappedTypeFullName}\"", method.Name, mappedType.FullName);
78+
return AdsErrorCode.DeviceServiceNotSupported;
79+
}
7080

71-
if (values.Length != rpcMethodToInvoke.GetType().GetMethod(nameof(IVerifyMethod.Verify))!.GetParameters().Length)
81+
if (parameterValues.Length != rpcMethodToInvoke.GetParameters().Length)
7282
{
83+
_logger?.LogError("Different method parameter length: {ParameterValuesLength} != {RpcMethodParameterLength}", parameterValues.Length, rpcMethodToInvoke.GetParameters().Length);
7384
return AdsErrorCode.DeviceInvalidParam;
7485
}
7586

76-
// FIXME: This is ugly...
77-
// Find a better solution, e.g. reflection.
78-
returnValue = rpcMethodToInvoke.Verify((string)values[0], (string)values[1], (string)values[2]);
87+
returnValue = rpcMethodToInvoke.Invoke(rpcMethodType, parameterValues);
7988

8089
return AdsErrorCode.NoError;
8190
}
8291

83-
private static RpcMethod BuildRpcMethod(MethodInfo methodInfo, IEnumerable<KeyValuePair<ParameterInfo, IDataType>> paramsKvp, KeyValuePair<ParameterInfo, IDataType> retValKvp)
92+
private static RpcMethod BuildRpcMethod(RpcMethodDescription rpcMethodDescription, IEnumerable<KeyValuePair<ParameterInfo, IDataType>> paramsKvp, KeyValuePair<ParameterInfo, IDataType> retValKvp)
8493
{
85-
var nameOrAlias = methodInfo.GetCustomAttribute<AliasAttribute>()?.AliasName ?? methodInfo.Name;
94+
var nameOrAlias = rpcMethodDescription.Alias ?? rpcMethodDescription.Method.Name;
8695
var rpc = new RpcMethod(nameOrAlias);
8796
foreach (var (k, v) in paramsKvp)
8897
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<TcPlcObject Version="1.1.0.1" ProductVersion="3.1.4024.13">
3+
<GVL Name="Global_Version" Id="{e8c1b537-7c98-47f8-8420-3bfa56237b19}">
4+
<Declaration><![CDATA[{attribute 'TcGenerated'}
5+
{attribute 'no-analysis'}
6+
{attribute 'linkalways'}
7+
// This function has been automatically generated from the project information.
8+
VAR_GLOBAL CONSTANT
9+
{attribute 'const_non_replaced'}
10+
stLibVersion_snappy : ST_LibVersion := (iMajor := 0, iMinor := 2, iBuild := 0, iRevision := 0, nFlags := 0, sVersion := '0.2.0.0');
11+
END_VAR
12+
]]></Declaration>
13+
</GVL>
14+
</TcPlcObject>

src/TcHaxx.Snappy/snappy/snappy.plcproj

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
<Company>TcHaxx</Company>
2020
<Released>false</Released>
2121
<Title>snappy</Title>
22-
<ProjectVersion>0.1.0.0</ProjectVersion>
22+
<ProjectVersion>0.2.0.0</ProjectVersion>
2323
<LibraryCategories>
2424
<LibraryCategory xmlns="">
2525
<Id>{9c7e50a7-dead-beef-897b-4cdbc169222d}</Id>
@@ -142,6 +142,9 @@
142142
<Compile Include="TESTs\snappy\FB_Snappy_Tests.TcPOU">
143143
<SubType>Code</SubType>
144144
</Compile>
145+
<Compile Include="Version\Global_Version.TcGVL">
146+
<SubType>Code</SubType>
147+
</Compile>
145148
</ItemGroup>
146149
<ItemGroup>
147150
<Folder Include="DUTs" />
@@ -156,6 +159,7 @@
156159
<Folder Include="TESTs" />
157160
<Folder Include="TESTs\snappy" />
158161
<Folder Include="TESTs\Serializer" />
162+
<Folder Include="Version" />
159163
<Folder Include="VISUs" />
160164
<Folder Include="POUs" />
161165
</ItemGroup>

0 commit comments

Comments
 (0)