diff --git a/src/SimConnect.NET/SimConnectAttribute.cs b/src/SimConnect.NET/SimConnectAttribute.cs
new file mode 100644
index 0000000..22c1e87
--- /dev/null
+++ b/src/SimConnect.NET/SimConnectAttribute.cs
@@ -0,0 +1,103 @@
+//
+// Copyright (c) BARS. All rights reserved.
+//
+
+using System;
+using SimConnect.NET.SimVar;
+
+namespace SimConnect.NET
+{
+ /// Annotates a struct field with the SimVar you want marshalled into it.
+ [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
+ public sealed class SimConnectAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class with name and unit.
+ /// The data type is inferred from the SimVar registry if available.
+ ///
+ /// The SimVar name to marshal.
+ /// The unit of the SimVar.
+ public SimConnectAttribute(string name, string unit)
+ {
+ this.Name = name;
+ this.Unit = unit;
+ var simVar = SimVarRegistry.Get(name);
+ if (simVar != null)
+ {
+ this.DataType = simVar.DataType;
+ }
+ else
+ {
+ throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class using the SimVar name.
+ /// The unit and data type are inferred from the SimVar registry if available.
+ ///
+ /// The SimVar name to marshal.
+ public SimConnectAttribute(string name)
+ {
+ this.Name = name;
+ var simVar = SimVarRegistry.Get(name);
+ if (simVar != null)
+ {
+ this.Unit = simVar.Unit;
+ this.DataType = simVar.DataType;
+ }
+ else
+ {
+ throw new ArgumentException($"SimVar '{name}' not found in registry. Please specify unit and dataType explicitly.", nameof(name));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The SimVar name to marshal.
+ /// The unit of the SimVar.
+ /// The SimConnect data type for marshaling.
+ public SimConnectAttribute(string name, string unit, SimConnectDataType dataType)
+ {
+ this.Name = name;
+ this.Unit = unit;
+ this.DataType = dataType;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The SimVar name to marshal.
+ /// The unit of the SimVar.
+ /// The SimConnect data type for marshaling.
+ /// The order in which the SimVar should be marshaled.
+ public SimConnectAttribute(string name, string? unit, SimConnectDataType dataType, int order)
+ {
+ this.Name = name;
+ this.Unit = unit;
+ this.DataType = dataType;
+ this.Order = order;
+ }
+
+ ///
+ /// Gets the SimVar name to marshal.
+ ///
+ public string Name { get; }
+
+ ///
+ /// Gets the unit of the SimVar.
+ ///
+ public string? Unit { get; }
+
+ ///
+ /// Gets the SimConnect data type for marshaling.
+ ///
+ public SimConnectDataType DataType { get; }
+
+ ///
+ /// Gets the order in which the SimVar should be marshaled.
+ ///
+ public int Order { get; }
+ }
+}
diff --git a/src/SimConnect.NET/SimVar/Internal/SimVarStructBinder.cs b/src/SimConnect.NET/SimVar/Internal/SimVarStructBinder.cs
new file mode 100644
index 0000000..2b150f3
--- /dev/null
+++ b/src/SimConnect.NET/SimVar/Internal/SimVarStructBinder.cs
@@ -0,0 +1,139 @@
+//
+// Copyright (c) BARS. All rights reserved.
+//
+
+using System;
+using System.Linq;
+using System.Reflection;
+
+namespace SimConnect.NET.SimVar.Internal
+{
+ internal static class SimVarStructBinder
+ {
+ ///
+ /// Returns the ordered [SimVar]-annotated fields for T, validating .NET types vs SimConnect types.
+ ///
+ internal static (System.Reflection.FieldInfo Field, SimConnectAttribute Attr)[] GetOrderedFields()
+ {
+ var t = typeof(T);
+ if (!t.IsLayoutSequential)
+ {
+ throw new InvalidOperationException($"{t.Name} must be annotated with [StructLayout(LayoutKind.Sequential)].");
+ }
+
+ var fields = t.GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
+ .Select(f => (Field: f, Attr: f.GetCustomAttribute()))
+ .Where(x => x.Attr != null)
+ .OrderBy(x => x!.Attr!.Order)
+ .ThenBy(x => x.Field.MetadataToken)
+ .ToArray();
+
+ if (fields.Length == 0)
+ {
+ throw new InvalidOperationException($"{t.Name} has no fields annotated with [SimVar].");
+ }
+
+ foreach (var (field, attr) in fields)
+ {
+ var ft = field.FieldType;
+ switch (attr!.DataType)
+ {
+ case SimConnectDataType.FloatDouble:
+ if (ft != typeof(double))
+ {
+ throw Fail(field, "double");
+ }
+
+ break;
+ case SimConnectDataType.FloatSingle:
+ if (ft != typeof(float))
+ {
+ throw Fail(field, "float");
+ }
+
+ break;
+ case SimConnectDataType.Integer32:
+ if (ft != typeof(int) && ft != typeof(uint))
+ {
+ throw Fail(field, "int/uint");
+ }
+
+ break;
+ case SimConnectDataType.Integer64:
+ if (ft != typeof(long) && ft != typeof(ulong))
+ {
+ throw Fail(field, "long/ulong");
+ }
+
+ break;
+ case SimConnectDataType.String8:
+ case SimConnectDataType.String32:
+ case SimConnectDataType.String64:
+ case SimConnectDataType.String128:
+ case SimConnectDataType.String256:
+ case SimConnectDataType.String260:
+ if (ft != typeof(string))
+ {
+ throw Fail(field, "string");
+ }
+
+ break;
+ }
+ }
+
+ return fields!;
+
+ static InvalidOperationException Fail(FieldInfo f, string expected)
+ => new($"Field {f.DeclaringType!.Name}.{f.Name} must be {expected} to match its [SimVar] attribute.");
+ }
+
+ ///
+ /// Builds a single SimConnect data definition for T (using [SimVar] attributes),
+ /// registers T for marshalling, and returns the definition ID.
+ ///
+ /// Native SimConnect handle.
+ internal static (uint DefId, (System.Reflection.FieldInfo Field, SimConnectAttribute Attr)[] Fields) BuildAndRegisterFromStruct(IntPtr handle)
+ {
+ var t = typeof(T);
+ if (!t.IsLayoutSequential)
+ {
+ throw new InvalidOperationException($"{t.Name} must be annotated with [StructLayout(LayoutKind.Sequential)].");
+ }
+
+ var fields = GetOrderedFields();
+
+ uint defId = unchecked((uint)Guid.NewGuid().GetHashCode());
+
+ var result = SimConnectNative.SimConnect_ClearDataDefinition(handle, defId);
+ if (result != 0)
+ {
+ throw new InvalidOperationException($"Failed to clear data definition for {t.Name}: {result}");
+ }
+
+ foreach (var (field, attr) in fields)
+ {
+ // Add each SimVar field to the SimConnect data definition using the native layer
+ if (attr == null)
+ {
+ throw new InvalidOperationException($"Field {field.Name} is missing [SimVar] attribute.");
+ }
+
+ result = SimConnectNative.SimConnect_AddToDataDefinition(
+ handle,
+ defId,
+ attr.Name,
+ attr.Unit ?? string.Empty,
+ (uint)attr.DataType);
+
+ if (result != 0)
+ {
+ throw new InvalidOperationException($"Failed to add data definition for {field.Name}: {result}");
+ }
+ }
+
+ var size = SimVarDataTypeSizing.GetPayloadSizeBytes(fields.Select(f => f.Attr!.DataType));
+ var offsets = SimVarDataTypeSizing.ComputeOffsets(fields.Select(f => f.Attr!.DataType));
+ return (defId, fields);
+ }
+ }
+}
diff --git a/src/SimConnect.NET/SimVar/SimVarDataTypeSizing.cs b/src/SimConnect.NET/SimVar/SimVarDataTypeSizing.cs
new file mode 100644
index 0000000..17cb7d4
--- /dev/null
+++ b/src/SimConnect.NET/SimVar/SimVarDataTypeSizing.cs
@@ -0,0 +1,77 @@
+//
+// Copyright (c) BARS. All rights reserved.
+//
+using SimConnect.NET;
+
+namespace SimConnect.NET.SimVar
+{
+ ///
+ /// Provides utilities for determining the size and offsets of SimConnect data types in unmanaged payloads.
+ ///
+ public static class SimVarDataTypeSizing
+ {
+ ///
+ /// Raw bytes for one datum of the given SimConnect type in the untagged payload.
+ ///
+ /// The SimConnect data type to evaluate.
+ /// The size in bytes of a single datum of the specified type.
+ public static int GetDatumSizeBytes(SimConnectDataType type) => type switch
+ {
+ SimConnectDataType.Invalid => 0,
+
+ // Scalars (raw sizes)
+ SimConnectDataType.Integer32 => 4, // uint (SimConnect uses unsigned 32-bit for Integer32)
+ SimConnectDataType.Integer64 => 8, // long
+ SimConnectDataType.FloatSingle => 4, // float
+ SimConnectDataType.FloatDouble => 8, // double
+
+ // Fixed-length strings (ANSI, fixed buffer including NUL)
+ SimConnectDataType.String8 => 8, // string
+ SimConnectDataType.String32 => 32, // string
+ SimConnectDataType.String64 => 64, // string
+ SimConnectDataType.String128 => 128, // string
+ SimConnectDataType.String256 => 256, // string
+ SimConnectDataType.String260 => 260, // string
+
+ // Not supported in this marshaller
+ SimConnectDataType.StringV => throw new NotSupportedException(
+ "StringV is not supported. Use fixed-length String8..String260."),
+
+ // Composite structs (per SDK)
+ SimConnectDataType.LatLonAlt => 24, // 3 x double
+ SimConnectDataType.Xyz => 24, // 3 x double
+ SimConnectDataType.InitPosition => 56, // 6 x double + 2 x DWORD (pack=1)
+
+ // These depend on your interop definition; use Marshal.SizeOf in your code.
+ SimConnectDataType.MarkerState => throw new NotSupportedException("Use Marshal.SizeOf()."),
+ SimConnectDataType.Waypoint => throw new NotSupportedException("Use Marshal.SizeOf()."),
+ _ => throw new ArgumentOutOfRangeException(nameof(type)),
+ };
+
+ ///
+ /// Total payload size (bytes) for a sequence of datums in untagged SIMOBJECT_DATA.
+ ///
+ /// The sequence of SimConnect data types to calculate the total payload size for.
+ /// The total size in bytes of the payload for the provided sequence of data types.
+ public static int GetPayloadSizeBytes(IEnumerable types)
+ => types.Sum(GetDatumSizeBytes);
+
+ ///
+ /// Compute byte offsets for each datum in order (untagged).
+ ///
+ /// The list of SimConnect data types to compute offsets for.
+ /// An array of byte offsets for each datum in the provided list.
+ public static int[] ComputeOffsets(IEnumerable types)
+ {
+ var offsets = new List();
+ int cursor = 0;
+ foreach (var type in types)
+ {
+ offsets.Add(cursor);
+ cursor += GetDatumSizeBytes(type);
+ }
+
+ return offsets.ToArray();
+ }
+ }
+}
diff --git a/src/SimConnect.NET/SimVar/SimVarManager.cs b/src/SimConnect.NET/SimVar/SimVarManager.cs
index 3526f73..cf42489 100644
--- a/src/SimConnect.NET/SimVar/SimVarManager.cs
+++ b/src/SimConnect.NET/SimVar/SimVarManager.cs
@@ -7,6 +7,7 @@
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
+using SimConnect.NET.SimVar.Internal;
namespace SimConnect.NET.SimVar
{
@@ -24,6 +25,9 @@ public sealed class SimVarManager : IDisposable
private readonly IntPtr simConnectHandle;
private readonly ConcurrentDictionary pendingRequests;
private readonly ConcurrentDictionary<(string Name, string Unit), uint> dataDefinitions;
+ private readonly ConcurrentDictionary defIndex = new();
+ private readonly ConcurrentDictionary typeToDefIndex = new();
+
private uint nextDefinitionId;
private uint nextRequestId;
private bool disposed;
@@ -141,6 +145,70 @@ public async Task SetAsync(string simVarName, string unit, T value, uint obje
await this.SetWithDefinitionAsync(dynamicDefinition, value, objectId, cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// Gets a full struct from SimConnect as a strongly-typed object using a dynamically built data definition.
+ ///
+ /// The struct type to request. Must be blittable/marshalable.
+ /// The SimConnect object ID (defaults to user aircraft).
+ /// Cancellation token for the operation.
+ /// A task that represents the asynchronous operation and returns the requested struct.
+ public async Task GetAsync(
+ uint objectId = SimConnectObjectIdUser,
+ CancellationToken cancellationToken = default)
+ {
+ ObjectDisposedException.ThrowIf(this.disposed, nameof(SimVarManager));
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (this.simConnectHandle == IntPtr.Zero)
+ {
+ throw new InvalidOperationException("SimConnect handle is not initialized.");
+ }
+
+ // Build definition for the struct using the native handle directly to avoid client coupling.
+ if (!this.typeToDefIndex.TryGetValue(typeof(T), out var defId))
+ {
+ var (newDefId, fields) = SimVarStructBinder.BuildAndRegisterFromStruct(this.simConnectHandle);
+ this.defIndex[newDefId] = fields;
+ this.typeToDefIndex[typeof(T)] = newDefId;
+ defId = newDefId;
+ }
+
+ var requestId = Interlocked.Increment(ref this.nextRequestId);
+ var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ this.pendingRequests[requestId] = tcs;
+
+ // Request data once for the specified object.
+ var hr = SimConnectNative.SimConnect_RequestDataOnSimObject(
+ this.simConnectHandle,
+ requestId,
+ defId,
+ objectId,
+ (uint)SimConnectPeriod.Once);
+
+ if (hr != (int)SimConnectError.None)
+ {
+ this.pendingRequests.TryRemove(requestId, out _);
+ throw new SimConnectException($"Failed to request struct {typeof(T).Name}: {(SimConnectError)hr}", (SimConnectError)hr);
+ }
+
+ using (cancellationToken.Register(() => tcs.TrySetCanceled(cancellationToken)))
+ {
+ // Apply optional manager timeout consistent with SimVar value requests
+ if (this.requestTimeout != Timeout.InfiniteTimeSpan)
+ {
+ var timeoutTask = Task.Delay(this.requestTimeout, CancellationToken.None);
+ var completed = await Task.WhenAny(tcs.Task, timeoutTask).ConfigureAwait(false);
+ if (completed == timeoutTask)
+ {
+ this.pendingRequests.TryRemove(requestId, out _);
+ throw new TimeoutException($"Struct request '{typeof(T).Name}' timed out after {this.requestTimeout} (RequestId={requestId})");
+ }
+ }
+
+ return await tcs.Task.ConfigureAwait(false);
+ }
+ }
+
///
/// Processes a received SimConnect message and completes any pending requests.
///
@@ -164,12 +232,61 @@ public void ProcessReceivedData(IntPtr data, uint dataSize)
var objectData = Marshal.PtrToStructure(data);
var requestId = objectData.RequestId;
- SimConnectLogger.Debug($"SimVar response received: RequestId={requestId}, DefineId={objectData.DefineId}, Size={recv.Size}");
-
if (this.pendingRequests.TryRemove(requestId, out var request))
{
- // Complete the request with the received data
- CompleteRequest(request, data, dataSize);
+ // If this is a simple SimVarRequest<>, use existing completion logic
+ if (request.GetType().IsGenericType && request.GetType().GetGenericTypeDefinition() == typeof(SimVarRequest<>))
+ {
+ CompleteRequest(request, data, dataSize);
+ }
+ else
+ {
+ // Otherwise, treat as a struct T TaskCompletionSource stored by GetAsync() struct overload
+ try
+ {
+ var definitionId = objectData.DefineId;
+ if (!this.defIndex.TryGetValue(definitionId, out var fields))
+ {
+ throw new InvalidOperationException($"No struct definition found for DefineId={definitionId}");
+ }
+ else
+ {
+ var headerSize = Marshal.SizeOf() - sizeof(ulong);
+ var dataPtr = IntPtr.Add(data, headerSize);
+ var currentPtr = dataPtr;
+ var tcsType = request.GetType();
+ if (tcsType.IsGenericType && tcsType.GetGenericTypeDefinition() == typeof(TaskCompletionSource<>))
+ {
+ var structType = tcsType.GetGenericArguments()[0];
+ var boxed = Activator.CreateInstance(structType)!;
+ foreach (var (field, attr) in fields)
+ {
+ var simConnectType = attr.DataType;
+ var fieldType = field.FieldType;
+ var simConnectSize = SimVarDataTypeSizing.GetDatumSizeBytes(simConnectType);
+ var fieldName = field.Name;
+ object? value = ParseType(currentPtr, simConnectType, fieldType);
+ field.SetValue(boxed, value);
+ currentPtr = IntPtr.Add(currentPtr, simConnectSize);
+ }
+
+ var trySetResultMethod = tcsType.GetMethod("TrySetResult");
+ trySetResultMethod?.Invoke(request, new[] { boxed });
+ }
+ else
+ {
+ SimConnectLogger.Warning("Unexpected TCS type for struct response.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ SimConnectLogger.Error("Error completing struct request", ex);
+ var tcsType = request.GetType();
+ var setEx = tcsType.GetMethod("TrySetException", new[] { typeof(Exception) });
+ setEx?.Invoke(request, new object[] { ex });
+ }
+ }
}
else
{
@@ -207,42 +324,17 @@ private static SimConnectDataType InferDataType()
{
var type = typeof(T);
- if (type == typeof(int) || type == typeof(bool))
- {
- return SimConnectDataType.Integer32;
- }
-
- if (type == typeof(long))
- {
- return SimConnectDataType.Integer64;
- }
-
- if (type == typeof(float))
- {
- return SimConnectDataType.FloatSingle;
- }
-
- if (type == typeof(double))
- {
- return SimConnectDataType.FloatDouble;
- }
-
- if (type == typeof(string))
+ return type switch
{
- return SimConnectDataType.String256; // Default string size
- }
-
- if (type == typeof(SimConnectDataLatLonAlt))
- {
- return SimConnectDataType.LatLonAlt;
- }
-
- if (type == typeof(SimConnectDataXyz))
- {
- return SimConnectDataType.Xyz;
- }
-
- throw new ArgumentException($"Unsupported type for SimVar: {type.Name}");
+ Type t when t == typeof(int) || t == typeof(bool) => SimConnectDataType.Integer32,
+ Type t when t == typeof(long) => SimConnectDataType.Integer64,
+ Type t when t == typeof(float) => SimConnectDataType.FloatSingle,
+ Type t when t == typeof(double) => SimConnectDataType.FloatDouble,
+ Type t when t == typeof(string) => SimConnectDataType.String256, // Default string size
+ Type t when t == typeof(SimConnectDataLatLonAlt) => SimConnectDataType.LatLonAlt,
+ Type t when t == typeof(SimConnectDataXyz) => SimConnectDataType.Xyz,
+ _ => throw new ArgumentException($"Unsupported type for SimVar: {type.Name}"),
+ };
}
private static bool IsTypeCompatible(Type requestedType, Type definitionType)
@@ -368,6 +460,7 @@ private static void CompleteRequest(object request, IntPtr data, uint dataSize)
// Parse the data based on the definition's data type
var parsedValue = ParseDataByType(dataPtr, definition.DataType, valueType);
+ // Use reflection to call SetResult directly, avoiding dynamic
var setResultMethod = requestType.GetMethod("SetResult");
setResultMethod?.Invoke(request, new[] { parsedValue });
}
@@ -447,6 +540,20 @@ private static object ParseFloat64Value(IntPtr dataPtr, Type expectedType)
return value;
}
+ private static object? ParseType(IntPtr dataPtr, SimConnectDataType simConnectType, Type fieldType)
+ {
+ return fieldType switch
+ {
+ Type t when t == typeof(int) => ParseInteger32(dataPtr),
+ Type t when t == typeof(long) => ParseInteger64(dataPtr),
+ Type t when t == typeof(float) => ParseFloat32(dataPtr),
+ Type t when t == typeof(double) => ParseFloat64(dataPtr),
+ Type t when t == typeof(string) => ParseString(dataPtr, simConnectType),
+ Type t when t == typeof(bool) => ParseInteger32(dataPtr) != 0,
+ _ => Marshal.PtrToStructure(dataPtr, fieldType),
+ };
+ }
+
private static double ParseFloat64(IntPtr dataPtr)
{
var bytes = Float64Bytes.Value!;
diff --git a/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs b/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs
index 7882910..66e6b49 100644
--- a/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs
+++ b/tests/SimConnect.NET.Tests.Net8/Tests/SimVarTests.cs
@@ -2,6 +2,7 @@
// Copyright (c) BARS. All rights reserved.
//
+using System.Runtime.InteropServices;
using SimConnect.NET;
namespace SimConnect.NET.Tests.Net8.Tests
@@ -90,6 +91,26 @@ private static async Task TestPositionSimVars(SimConnectClient client, Can
return false;
}
+ var position = await client.SimVars.GetAsync(cancellationToken: cancellationToken);
+ Console.WriteLine($" 🗺️ Position struct: {position.Latitude:F6}°, {position.Longitude:F6}°, {position.Altitude:F0}ft");
+ if (position.Latitude < -90 || position.Latitude > 90)
+ {
+ Console.WriteLine(" ❌ Invalid latitude value");
+ return false;
+ }
+
+ if (position.Longitude < -180 || position.Longitude > 180)
+ {
+ Console.WriteLine(" ❌ Invalid longitude value");
+ return false;
+ }
+
+ if (position.Altitude < 0 || position.Altitude > 60000)
+ {
+ Console.WriteLine(" ❌ Invalid altitude value");
+ return false;
+ }
+
return true;
}
@@ -158,3 +179,27 @@ private static async Task TestRapidRequests(SimConnectClient client, Cance
}
}
}
+
+///
+/// Represents the aircraft position using SimVars.
+///
+public struct Position
+{
+ ///
+ /// Gets or sets the latitude of the plane in degrees.
+ ///
+ [SimConnect("PLANE LATITUDE", "degrees")]
+ public double Latitude;
+
+ ///
+ /// Gets or sets the longitude of the plane in degrees.
+ ///
+ [SimConnect("PLANE LONGITUDE", "degrees")]
+ public double Longitude;
+
+ ///
+ /// Gets or sets the altitude of the plane in feet.
+ ///
+ [SimConnect("PLANE ALTITUDE", "feet")]
+ public double Altitude;
+}