Add the derive proc macro

This commit is contained in:
Mihai Dinculescu
2022-10-10 19:33:49 +01:00
parent 2f845f1c8f
commit 9b7e3b97db
29 changed files with 951 additions and 96 deletions

59
Cargo.lock generated
View File

@@ -107,6 +107,12 @@ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
[[package]]
name = "dissimilar"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c97b9233581d84b8e1e689cdd3a47b6f69770084fc246e86a7f78b0d9c1d4a5"
[[package]] [[package]]
name = "either" name = "either"
version = "1.8.0" version = "1.8.0"
@@ -163,6 +169,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "itoa"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@@ -353,12 +365,40 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.145" version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
[[package]]
name = "serde_derive"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.4" version = "0.1.4"
@@ -389,9 +429,12 @@ dependencies = [
name = "simconnect-sdk-derive" name = "simconnect-sdk-derive"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"simconnect-sdk",
"syn", "syn",
"trybuild",
] ]
[[package]] [[package]]
@@ -541,6 +584,22 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "trybuild"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea496675d71016e9bc76aa42d87f16aefd95447cc5818e671e12b2d7e269075d"
dependencies = [
"dissimilar",
"glob",
"once_cell",
"serde",
"serde_derive",
"serde_json",
"termcolor",
"toml",
]
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.4" version = "1.0.4"

View File

@@ -6,4 +6,5 @@
git clone git@github.com:mihai-dinculescu/simconnect-sdk.git git clone git@github.com:mihai-dinculescu/simconnect-sdk.git
cd simconnect-sdk cd simconnect-sdk
RUST_LOG=info cargo run --bin basic RUST_LOG=info cargo run --bin basic
RUST_LOG=info cargo run --bin basic_no_macros
``` ```

View File

@@ -5,13 +5,18 @@ authors = ["Mihai Dinculescu <mihai.dinculescu@outlook.com>"]
edition = "2021" edition = "2021"
description = "SimConnect SDK Examples" description = "SimConnect SDK Examples"
license = "MIT" license = "MIT"
publish = false
[[bin]] [[bin]]
name = "basic" name = "basic"
path = "src/basic.rs" path = "src/basic.rs"
[[bin]]
name = "basic_no_macros"
path = "src/basic_no_macros.rs"
[dependencies] [dependencies]
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [ "env-filter" ] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
simconnect-sdk = { path = "../simconnect-sdk" } simconnect-sdk = { path = "../simconnect-sdk", features = ["derive"] }

View File

@@ -1,66 +1,29 @@
use simconnect_sdk::{ #![allow(dead_code)]
ConditionEnum, DataType, Notification, NotificationData, PeriodEnum, SimConnect,
SimConnectError, use simconnect_sdk::{Notification, SimConnect, SimConnectObject};
}; use simconnect_sdk_examples::setup_logging;
use tracing::{error, info}; use tracing::{error, info};
use simconnect_sdk_examples::setup_logging; #[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second")]
#[derive(Debug, Clone)] struct GpsData {
pub struct GpsData { #[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
pub lat: f64, lat: f64,
pub lon: f64, #[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
pub alt: f64, lon: f64,
pub gps_ground_magnetic_track: f64, #[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
pub gps_ground_speed: f64, alt: f64,
#[simconnect(name = "GPS GROUND MAGNETIC TRACK", unit = "degrees")]
gps_ground_magnetic_track: f64,
#[simconnect(name = "GPS GROUND SPEED", unit = "Meters per second")]
gps_ground_speed: f64,
} }
impl simconnect_sdk::SimConnectObjectExt for GpsData { #[derive(Debug, Clone, SimConnectObject)]
fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError> { #[simconnect(period = "second", condition = "changed")]
client.add_to_data_definition(id, "PLANE LATITUDE", "degrees", DataType::F64)?;
client.add_to_data_definition(id, "PLANE LONGITUDE", "degrees", DataType::F64)?;
client.add_to_data_definition(id, "PLANE ALTITUDE", "meters", DataType::F64)?;
client.add_to_data_definition(id, "GPS GROUND MAGNETIC TRACK", "degrees", DataType::F64)?;
client.add_to_data_definition(
id,
"GPS GROUND SPEED",
"Meters per second",
DataType::F64,
)?;
client.request_data_on_sim_object(id, PeriodEnum::Second, ConditionEnum::None)?;
Ok(())
}
}
impl TryFrom<&NotificationData> for GpsData {
type Error = ();
fn try_from(value: &NotificationData) -> Result<Self, Self::Error> {
value.try_into::<GpsData>().ok_or(())
}
}
#[derive(Debug, Clone)]
pub struct OnGround { pub struct OnGround {
pub sim_on_ground: bool, #[simconnect(name = "SIM ON GROUND", unit = "bool")]
} sim_on_ground: bool,
impl simconnect_sdk::SimConnectObjectExt for OnGround {
fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError> {
client.add_to_data_definition(id, "SIM ON GROUND", "bool", DataType::Bool)?;
client.request_data_on_sim_object(id, PeriodEnum::Second, ConditionEnum::None)?;
Ok(())
}
}
impl TryFrom<&NotificationData> for OnGround {
type Error = ();
fn try_from(value: &NotificationData) -> Result<Self, Self::Error> {
value.try_into::<OnGround>().ok_or(())
}
} }
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -81,11 +44,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
Some(Notification::Data(data)) => { Some(Notification::Data(data)) => {
if let Ok(gps_data) = GpsData::try_from(&data) { if let Ok(gps_data) = GpsData::try_from(&data) {
info!("GPS Data: {:?}", gps_data); info!("GPS Data: {gps_data:?}");
continue; continue;
} }
if let Ok(on_ground) = OnGround::try_from(&data) { if let Ok(on_ground) = OnGround::try_from(&data) {
info!("On Ground data: {:?}", on_ground); info!("On Ground data: {on_ground:?}");
continue; continue;
} }
} }

View File

@@ -0,0 +1,108 @@
#![allow(dead_code)]
use simconnect_sdk::{
Condition, DataType, Notification, NotificationData, Period, SimConnect, SimConnectError,
SimConnectObjectExt,
};
use tracing::{error, info};
use simconnect_sdk_examples::setup_logging;
#[derive(Debug, Clone)]
pub struct GpsData {
lat: f64,
lon: f64,
alt: f64,
gps_ground_magnetic_track: f64,
gps_ground_speed: f64,
}
impl SimConnectObjectExt for GpsData {
fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError> {
client.add_to_data_definition(id, "PLANE LATITUDE", "degrees", DataType::Float64)?;
client.add_to_data_definition(id, "PLANE LONGITUDE", "degrees", DataType::Float64)?;
client.add_to_data_definition(id, "PLANE ALTITUDE", "meters", DataType::Float64)?;
client.add_to_data_definition(
id,
"GPS GROUND MAGNETIC TRACK",
"degrees",
DataType::Float64,
)?;
client.add_to_data_definition(
id,
"GPS GROUND SPEED",
"Meters per second",
DataType::Float64,
)?;
client.request_data_on_sim_object(id, Period::Second, Condition::None, 0)?;
Ok(())
}
}
impl TryFrom<&NotificationData> for GpsData {
type Error = SimConnectError;
fn try_from(value: &NotificationData) -> Result<Self, Self::Error> {
value.try_transmute::<GpsData>()
}
}
#[derive(Debug, Clone)]
pub struct OnGround {
sim_on_ground: bool,
}
impl SimConnectObjectExt for OnGround {
fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError> {
client.add_to_data_definition(id, "SIM ON GROUND", "bool", DataType::Bool)?;
client.request_data_on_sim_object(id, Period::Second, Condition::Changed, 0)?;
Ok(())
}
}
impl TryFrom<&NotificationData> for OnGround {
type Error = SimConnectError;
fn try_from(value: &NotificationData) -> Result<Self, Self::Error> {
value.try_transmute::<OnGround>()
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
setup_logging()?;
let client = SimConnect::new("Simple Program");
match client {
Ok(mut client) => loop {
let notification = client.get_next_dispatch()?;
match notification {
Some(Notification::Open) => {
info!("Open");
client.register_object::<GpsData>()?;
client.register_object::<OnGround>()?;
}
Some(Notification::Data(data)) => {
if let Ok(gps_data) = GpsData::try_from(&data) {
info!("GPS Data: {gps_data:?}");
continue;
}
if let Ok(on_ground) = OnGround::try_from(&data) {
info!("On Ground data: {on_ground:?}");
continue;
}
}
_ => (),
}
},
Err(e) => {
error!("{e:?}")
}
}
Ok(())
}

View File

@@ -5,11 +5,22 @@ authors = ["Mihai Dinculescu <mihai.dinculescu@outlook.com>"]
edition = "2021" edition = "2021"
description = "Macros of SimConnect SDK for Rust" description = "Macros of SimConnect SDK for Rust"
license = "MIT" license = "MIT"
autotests = false
publish = false
[lib] [lib]
proc-macro = true proc-macro = true
[[test]]
name = "tests"
path = "tests/run.rs"
[dependencies] [dependencies]
proc-macro2 = "1.0" proc-macro2 = "1.0"
quote = "1.0" quote = "1.0"
syn = "1.0" syn = "1.0"
once_cell = "1.7"
[dev-dependencies]
simconnect-sdk = { path = "../simconnect-sdk" }
trybuild = { version = "1.0", features = ["diff"] }

View File

@@ -1,7 +1,341 @@
extern crate proc_macro; extern crate proc_macro;
use proc_macro::TokenStream; use std::collections::HashMap;
#[proc_macro_derive(SimConnectObject, attributes(field))] use once_cell::sync::Lazy;
pub fn derive(_: TokenStream) -> TokenStream { use proc_macro::TokenStream;
TokenStream::new() use quote::quote;
use syn::{parse_macro_input, DeriveInput};
struct FieldInfo {
required: bool,
accepted_values: Vec<String>,
}
static ALLOWED_CLASS_ATTRIBUTES: Lazy<HashMap<String, FieldInfo>> = Lazy::new(|| {
let mut map = HashMap::new();
map.insert(
"period".to_string(),
FieldInfo {
required: true,
accepted_values: vec![
"once".to_string(),
"visual-frame".to_string(),
"sim-frame".to_string(),
"second".to_string(),
],
},
);
map.insert(
"condition".to_string(),
FieldInfo {
required: false,
accepted_values: vec!["none".to_string(), "changed".to_string()],
},
);
map
});
static ALLOWED_FIELD_ATTRIBUTES: Lazy<HashMap<String, FieldInfo>> = Lazy::new(|| {
let mut map = HashMap::new();
map.insert(
"name".to_string(),
FieldInfo {
required: true,
accepted_values: vec![],
},
);
map.insert(
"unit".to_string(),
FieldInfo {
required: true,
accepted_values: vec![],
},
);
map
});
const SUPPORTED_FIELD_TYPES: [&str; 2] = ["f64", "bool"];
/// SimConnectObject derive macro.
///
/// # Struct Arguments
/// * `period` - Required. One of `once`, `visual-frame`, `sim-frame`, `second`.
/// * `condition` - Optional. The condition of the data. Must be either `none` or `changed`. Defaults to `none`.
///
/// # Field Arguments
/// * `name` - Required. The name of the field. One from <http://www.prepar3d.com/SDKv3/LearningCenter/utilities/variables/simulation_variables.html#Simulation%20Variables>.
/// * `unit` - Required. The unit of the field.
///
/// # Example
/// ```rust
/// use simconnect_sdk::SimConnectObject;
///
/// #[derive(Debug, Clone, SimConnectObject)]
/// #[simconnect(period = "second")]
/// struct GpsData {
/// #[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
/// lat: f64,
/// #[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
/// lon: f64,
/// #[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
/// alt: f64,
/// }
/// ```
#[proc_macro_derive(SimConnectObject, attributes(simconnect))]
pub fn derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let fields = if let syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
return mk_err(
ast,
"Unsupported field type. Only named fields are supported.",
)
.into();
};
let build_fields = fields.iter().map(parse_field);
let request_data = request_data(&ast);
let expanded = quote! {
impl simconnect_sdk::SimConnectObjectExt for #name {
fn register(client: &mut simconnect_sdk::SimConnect, id: u32) -> Result<(), simconnect_sdk::SimConnectError> {
#(#build_fields)*
#request_data
Ok(())
}
}
impl TryFrom<&simconnect_sdk::NotificationData> for #name {
type Error = simconnect_sdk::SimConnectError;
fn try_from(value: &simconnect_sdk::NotificationData) -> Result<Self, Self::Error> {
value.try_transmute::<#name>()
}
}
};
expanded.into()
}
fn parse_field(f: &syn::Field) -> proc_macro2::TokenStream {
let error_message = "expected attribute `#[simconnect(name = \"...\", unit = \"...\")]`";
let attr = get_attribute(&f.attrs);
match attr {
Some(attr) => {
let ty = &f.ty;
let properties =
extract_attribute_string_properties(attr, &ALLOWED_FIELD_ATTRIBUTES, error_message);
match properties {
Ok(properties) => {
let error_message_supported_types = &format!(
"Field type must be one of ['{}']",
SUPPORTED_FIELD_TYPES.join("', '")
);
match ty {
syn::Type::Path(syn::TypePath { path, .. }) => {
let path = &path.segments;
let path = path.iter().map(|s| &s.ident);
let name = properties.get("name").expect("this should never happen");
let unit = properties.get("unit").expect("this should never happen");
match path.last() {
Some(value) if value == "f64" => {
quote! {
client.add_to_data_definition(id, #name, #unit, simconnect_sdk::DataType::Float64)?;
}
}
Some(value) if value == "bool" => {
quote! {
client.add_to_data_definition(id, #name, #unit, simconnect_sdk::DataType::Bool)?;
}
}
_ => mk_err(f, error_message_supported_types),
}
}
_ => mk_err(f, error_message_supported_types),
}
}
Err(e) => e,
}
}
None => mk_err(f, error_message),
}
}
fn request_data(ast: &DeriveInput) -> proc_macro2::TokenStream {
let attr = get_attribute(&ast.attrs);
let error_message = "expected attribute `#[simconnect(period = \"...\", condition = \"...\")]`";
match attr {
Some(attr) => {
let properties =
extract_attribute_string_properties(attr, &ALLOWED_CLASS_ATTRIBUTES, error_message);
match properties {
Ok(properties) => {
let period = match properties.get("period") {
Some(p) if p == "once" => {
quote! {
simconnect_sdk::Period::Once
}
}
Some(p) if p == "visual-frame" => {
quote! {
simconnect_sdk::Period::VisualFrame
}
}
Some(p) if p == "sim-frame" => {
quote! {
simconnect_sdk::Period::SimFrame
}
}
_ => {
quote! {
simconnect_sdk::Period::Second
}
}
};
let condition = match properties.get("condition") {
Some(c) if c == "changed" => {
quote! {
simconnect_sdk::Condition::Changed
}
}
_ => {
quote! {
simconnect_sdk::Condition::None
}
}
};
quote! {
client.request_data_on_sim_object(id, #period, #condition, 0)?;
}
}
Err(e) => e,
}
}
None => mk_err(ast, error_message),
}
}
fn get_attribute(attrs: &[syn::Attribute]) -> Option<&syn::Attribute> {
attrs
.iter()
.find(|&attr| attr.path.segments.len() == 1 && attr.path.segments[0].ident == "simconnect")
}
fn extract_attribute_string_properties(
attr: &syn::Attribute,
allowed_properties: &HashMap<String, FieldInfo>,
error_message: &str,
) -> Result<HashMap<String, String>, proc_macro2::TokenStream> {
let mut results = HashMap::new();
match attr.parse_meta() {
Ok(syn::Meta::List(nvs)) => {
for item in nvs.nested.iter() {
match &item {
syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
match nv.path.get_ident() {
Some(ident) => {
let ident_string = ident.to_string();
let allowed_property = allowed_properties.get(&ident_string);
match allowed_property {
Some(property) => {
if results.contains_key(&ident_string) {
// found a duplicate property name
return Err(mk_err(nvs.clone(), error_message));
}
match &nv.lit {
syn::Lit::Str(s) => {
let value = s.value();
if !property.accepted_values.is_empty()
&& !property.accepted_values.contains(&value)
{
// found an invalid value
return Err(mk_err(
nv,
&format!(
"`{ident_string}` must be one of ['{}']",
property.accepted_values.join("', '")
),
));
}
results.insert(ident_string, value);
}
lit => {
return Err(syn::Error::new_spanned(
nv,
format!("expected string, found {lit:?}"),
)
.to_compile_error())
}
}
}
None => {
// found an unexpected property name
return Err(mk_err(nvs.clone(), error_message));
}
}
}
None => {
// no ident found
return Err(mk_err(nvs.clone(), error_message));
}
}
}
meta => {
// nvc.nested[] was not k = v
return Err(mk_err(meta, error_message));
}
}
}
// check that all required properties are specified
for (field, _) in allowed_properties.iter().filter(|(_, fi)| fi.required) {
if !results.contains_key(field) {
return Err(mk_err(nvs, error_message));
}
}
}
Ok(meta) => {
// inside of #[] there was just an identifier (`#[simconnect]`)
// or a key-value mapping (`#[simconnect = "foo"]`), neither of which are okay.
return Err(mk_err(meta, error_message));
}
Err(e) => {
return Err(e.to_compile_error());
}
};
Ok(results)
}
fn mk_err<T: quote::ToTokens>(t: T, message: &str) -> proc_macro2::TokenStream {
syn::Error::new_spanned(t, message).to_compile_error()
} }

View File

@@ -0,0 +1,31 @@
#![allow(unused_variables, dead_code)]
use simconnect_sdk_derive::SimConnectObject;
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second")]
struct GpsData1 {
#[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
pub lat: f64,
#[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
pub lon: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData2 {
#[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
pub lat: f64,
#[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
pub lon: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "visual-frame", condition = "changed")]
struct GpsData3 {
#[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
pub lat: f64,
#[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
pub lon: f64,
}
fn main() {}

View File

@@ -0,0 +1,39 @@
#![allow(unused_variables, dead_code)]
use simconnect_sdk_derive::SimConnectObject;
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "visual-frame", condition = "changed")]
struct GpsData1(f64);
#[derive(Debug, Clone, SimConnectObject)]
struct GpsData2 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect]
struct GpsData3 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect()]
struct GpsData4 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", period = "second")]
struct GpsData5 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(condition = "none", condition = "none")]
struct GpsData6 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none", test = "test")]
struct GpsData7 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(periodX = "second", condition = "none")]
struct GpsData8 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", conditionX = "none")]
struct GpsData9 {}
fn main() {}

View File

@@ -0,0 +1,54 @@
error: Unsupported field type. Only named fields are supported.
--> tests/02-struct-attr-errors.rs:5:1
|
5 | / #[simconnect(period = "visual-frame", condition = "changed")]
6 | | struct GpsData1(f64);
| |_____________________^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:9:1
|
9 | struct GpsData2 {}
| ^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:12:3
|
12 | #[simconnect]
| ^^^^^^^^^^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:16:3
|
16 | #[simconnect()]
| ^^^^^^^^^^^^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:20:3
|
20 | #[simconnect(period = "second", period = "second")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:24:3
|
24 | #[simconnect(condition = "none", condition = "none")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:28:3
|
28 | #[simconnect(period = "second", condition = "none", test = "test")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:32:3
|
32 | #[simconnect(periodX = "second", condition = "none")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(period = "...", condition = "...")]`
--> tests/02-struct-attr-errors.rs:36:3
|
36 | #[simconnect(period = "second", conditionX = "none")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1,73 @@
#![allow(unused_variables, dead_code)]
use simconnect_sdk_derive::SimConnectObject;
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData1 {
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData2 {
#[simconnect]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData3 {
#[simconnect()]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData4 {
#[simconnect(name = "PLANE LATITUDE")]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData5 {
#[simconnect(unit = "degrees")]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData6 {
#[simconnect(name = "PLANE LATITUDE", name = "PLANE LATITUDE")]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData7 {
#[simconnect(unit = "degrees", unit = "degrees")]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData8 {
#[simconnect(name = "PLANE LATITUDE", unit = "degrees", unit = "degrees")]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData9 {
#[simconnect(nameX = "PLANE LATITUDE", unit = "degrees")]
pub lat: f64,
}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData10 {
#[simconnect(name = "PLANE LATITUDE", unitX = "degrees")]
pub lat: f64,
}
fn main() {}

View File

@@ -0,0 +1,59 @@
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:7:5
|
7 | pub lat: f64,
| ^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:13:7
|
13 | #[simconnect]
| ^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:20:7
|
20 | #[simconnect()]
| ^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:27:7
|
27 | #[simconnect(name = "PLANE LATITUDE")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:34:7
|
34 | #[simconnect(unit = "degrees")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:41:7
|
41 | #[simconnect(name = "PLANE LATITUDE", name = "PLANE LATITUDE")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:48:7
|
48 | #[simconnect(unit = "degrees", unit = "degrees")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:55:7
|
55 | #[simconnect(name = "PLANE LATITUDE", unit = "degrees", unit = "degrees")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:62:7
|
62 | #[simconnect(nameX = "PLANE LATITUDE", unit = "degrees")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: expected attribute `#[simconnect(name = "...", unit = "...")]`
--> tests/03-field-attr-errors.rs:69:7
|
69 | #[simconnect(name = "PLANE LATITUDE", unitX = "degrees")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -0,0 +1,27 @@
#![allow(unused_variables, dead_code)]
use simconnect_sdk_derive::SimConnectObject;
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = 123, condition = "none")]
struct GpsData1 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = 123)]
struct GpsData2 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "X")]
struct GpsData3 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "X")]
struct GpsData4 {}
#[derive(Debug, Clone, SimConnectObject)]
#[simconnect(period = "second", condition = "none")]
struct GpsData5 {
#[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
pub lat: String,
}
fn main() {}

View File

@@ -0,0 +1,30 @@
error: expected string, found Int(LitInt { token: 123 })
--> tests/04-invalid-values.rs:5:14
|
5 | #[simconnect(period = 123, condition = "none")]
| ^^^^^^^^^^^^
error: expected string, found Int(LitInt { token: 123 })
--> tests/04-invalid-values.rs:9:33
|
9 | #[simconnect(period = "second", condition = 123)]
| ^^^^^^^^^^^^^^^
error: `period` must be one of ['once', 'visual-frame', 'sim-frame', 'second']
--> tests/04-invalid-values.rs:13:14
|
13 | #[simconnect(period = "X")]
| ^^^^^^^^^^^^
error: `condition` must be one of ['none', 'changed']
--> tests/04-invalid-values.rs:17:33
|
17 | #[simconnect(period = "second", condition = "X")]
| ^^^^^^^^^^^^^^^
error: Field type must be one of ['f64', 'bool']
--> tests/04-invalid-values.rs:23:5
|
23 | / #[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
24 | | pub lat: String,
| |___________________^

View File

@@ -0,0 +1,8 @@
#[test]
fn tests() {
let t = trybuild::TestCases::new();
t.pass("tests/01-parse.rs");
t.compile_fail("tests/02-struct-attr-errors.rs");
t.compile_fail("tests/03-field-attr-errors.rs");
t.compile_fail("tests/04-invalid-values.rs");
}

View File

@@ -6,6 +6,10 @@ edition = "2021"
description = "SimConnect SDK for Rust" description = "SimConnect SDK for Rust"
license = "MIT" license = "MIT"
[features]
default = []
derive = ["simconnect-sdk-derive"]
[build-dependencies] [build-dependencies]
bindgen = "0.60" bindgen = "0.60"
@@ -13,4 +17,4 @@ bindgen = "0.60"
num_enum = "0.5" num_enum = "0.5"
tracing = "0.1" tracing = "0.1"
thiserror = "1.0" thiserror = "1.0"
simconnect-sdk-derive = { path = "../simconnect-sdk-derive" } simconnect-sdk-derive = { path = "../simconnect-sdk-derive", optional = true }

View File

@@ -1,3 +1,4 @@
/// Airport data.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct AirportData { pub struct AirportData {
pub icao: String, pub icao: String,

View File

@@ -1,5 +1,8 @@
/// Specifies under which conditions the data is to be sent by the server and received by the client.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ConditionEnum { pub enum Condition {
/// The default, data will be sent strictly according to the defined period.
None, None,
/// Data will only be sent to the client when one or more values have changed.
Changed, Changed,
} }

View File

@@ -1,5 +1,6 @@
/// SimConnect object property data type.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum DataType { pub enum DataType {
F64, Float64,
Bool, Bool,
} }

View File

@@ -1,5 +1,7 @@
use std::os::raw::c_char; use std::os::raw::c_char;
/// SimConnect event.
/// As defined at <https://www.prepar3d.com/SDKv3/LearningCenter/utilities/variables/event_ids.html>
#[derive(Debug, Copy, Clone, num_enum::TryFromPrimitive)] #[derive(Debug, Copy, Clone, num_enum::TryFromPrimitive)]
#[repr(u32)] #[repr(u32)]
pub enum Event { pub enum Event {

View File

@@ -1,5 +0,0 @@
#[derive(Copy, Clone)]
#[repr(u32)]
pub enum Group {
Group0,
}

View File

@@ -2,14 +2,14 @@ mod airport_data;
mod condition; mod condition;
mod data_type; mod data_type;
mod event; mod event;
mod group;
mod notification; mod notification;
mod notification_group;
mod period; mod period;
pub use airport_data::*; pub use airport_data::*;
pub use condition::*; pub use condition::*;
pub use data_type::*; pub use data_type::*;
pub use event::*; pub use event::*;
pub use group::*;
pub use notification::*; pub use notification::*;
pub use notification_group::*;
pub use period::*; pub use period::*;

View File

@@ -1,28 +1,39 @@
use crate::{AirportData, Event, SimConnectObjectExt}; use crate::{AirportData, Event, SimConnectError, SimConnectObjectExt};
/// Notification received from SimConnect.
pub enum Notification { pub enum Notification {
/// SimConnect open
Open, Open,
/// SimConnect event
Event(Event), Event(Event),
/// SimConnect object
Data(NotificationData), Data(NotificationData),
/// SimConnect airport list
AirportList(Vec<AirportData>), AirportList(Vec<AirportData>),
/// SimConnect quit
Quit, Quit,
/// SimConnect exception
Exception(u32), Exception(u32),
} }
/// Notification data object.
pub struct NotificationData { pub struct NotificationData {
pub(crate) type_id: String, pub(crate) type_id: String,
pub(crate) data_addr: *const u32, pub(crate) data_addr: *const u32,
} }
impl NotificationData { impl NotificationData {
pub fn try_into<T: SimConnectObjectExt>(&self) -> Option<T> { pub fn try_transmute<T: SimConnectObjectExt>(&self) -> Result<T, SimConnectError> {
let type_id: String = std::any::type_name::<T>().into(); let type_id: String = std::any::type_name::<T>().into();
if self.type_id == type_id { if self.type_id == type_id {
let data: &T = unsafe { std::mem::transmute_copy(&self.data_addr) }; let data: &T = unsafe { std::mem::transmute_copy(&self.data_addr) };
Some(data.clone()) Ok(data.clone())
} else { } else {
None Err(SimConnectError::ObjectMismatch {
actual: self.type_id.clone(),
expected: type_id,
})
} }
} }
} }

View File

@@ -0,0 +1,6 @@
/// SimConnect event notification group.
#[derive(Copy, Clone)]
#[repr(u32)]
pub enum NotificationGroup {
Group0,
}

View File

@@ -1,5 +1,16 @@
/// Specifies how often the data is to be sent by the server and received by the client.
/// 0 - every interval.
/// 1 - every other interval.
/// 2 - every third interval.
/// etc.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PeriodEnum { pub enum Period {
VisualFrame { interval: u32 }, /// Specifies that the data should be sent once only. Note that this is not an efficient way of receiving data frequently, use one of the other periods if there is a regular frequency to the data request.
Once,
/// Specifies that the data should be sent every visual (rendered) frame.
VisualFrame,
/// Specifies that the data should be sent every simulated frame, whether that frame is rendered or not.
SimFrame,
/// Specifies that the data should be sent once every second.
Second, Second,
} }

View File

@@ -1,15 +1,24 @@
use thiserror::Error; use thiserror::Error;
/// SimConnect SDK error.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum SimConnectError { pub enum SimConnectError {
/// SimConnect error.
#[error("SimConnect error: {0}")] #[error("SimConnect error: {0}")]
SimConnectError(i32), SimConnectError(i32),
#[error("SimConnect unrecognized: {0}")] #[error("SimConnect unrecognized: {0}")]
/// SimConnect unrecognized error. Occurs when an unimplemented event is received by the SDK.
SimConnectUnrecognizedEvent(u32), SimConnectUnrecognizedEvent(u32),
/// Object already registered with the client instance.
#[error("Object `{0}` has already been registered")] #[error("Object `{0}` has already been registered")]
ObjectAlreadyRegistered(String), ObjectAlreadyRegistered(String),
/// Object mismatch.
#[error("Tried to convert object of type {actual} to {expected}")]
ObjectMismatch { actual: String, expected: String },
/// Conversation error.
#[error("Conversion error: {0}")] #[error("Conversion error: {0}")]
ConversionError(#[from] std::num::TryFromIntError), ConversionError(#[from] std::num::TryFromIntError),
/// Unexpected error.
#[error("Unexpected error: {0}")] #[error("Unexpected error: {0}")]
UnexpectedError(String), UnexpectedError(String),
} }

View File

@@ -1,3 +1,8 @@
//! # SimConnect SDK
//! SimConnect SDK in Rust.
//!
//! See [examples](https://github.com/mihai-dinculescu/simconnect-sdk/tree/main/examples/src).
mod bindings; mod bindings;
mod domain; mod domain;
mod errors; mod errors;
@@ -13,5 +18,7 @@ pub use errors::SimConnectError;
pub use simconnect::SimConnect; pub use simconnect::SimConnect;
pub use simconnect_object_ext::SimConnectObjectExt; pub use simconnect_object_ext::SimConnectObjectExt;
#[cfg(feature = "simconnect-sdk-derive")]
extern crate simconnect_sdk_derive; extern crate simconnect_sdk_derive;
#[cfg(feature = "simconnect-sdk-derive")]
pub use simconnect_sdk_derive::*; pub use simconnect_sdk_derive::*;

View File

@@ -2,10 +2,11 @@ use std::ffi::c_void;
use crate::{ use crate::{
as_c_string, bindings, helpers::fixed_c_str_to_string, ok_if_fail, success, AirportData, as_c_string, bindings, helpers::fixed_c_str_to_string, ok_if_fail, success, AirportData,
ConditionEnum, DataType, Event, Group, Notification, NotificationData, PeriodEnum, Condition, DataType, Event, Notification, NotificationData, NotificationGroup, Period,
SimConnectError, SimConnectObjectExt, SimConnectError, SimConnectObjectExt,
}; };
/// SimConnect SDK Client.
#[derive(Debug)] #[derive(Debug)]
pub struct SimConnect { pub struct SimConnect {
pub handle: std::ptr::NonNull<c_void>, pub handle: std::ptr::NonNull<c_void>,
@@ -71,7 +72,7 @@ impl SimConnect {
) )
}); });
let group = Group::Group0; let group = NotificationGroup::Group0;
success!(unsafe { success!(unsafe {
bindings::SimConnect_AddClientEventToNotificationGroup( bindings::SimConnect_AddClientEventToNotificationGroup(
@@ -98,7 +99,7 @@ impl SimConnect {
data_type: DataType, data_type: DataType,
) -> Result<(), SimConnectError> { ) -> Result<(), SimConnectError> {
let c_type = match data_type { let c_type = match data_type {
DataType::F64 => bindings::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_FLOAT64, DataType::Float64 => bindings::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_FLOAT64,
DataType::Bool => bindings::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_INT32, DataType::Bool => bindings::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_INT32,
}; };
@@ -121,21 +122,22 @@ impl SimConnect {
pub fn request_data_on_sim_object( pub fn request_data_on_sim_object(
&self, &self,
request_id: u32, request_id: u32,
period: PeriodEnum, period: Period,
condition: ConditionEnum, condition: Condition,
interval: u32,
) -> Result<(), SimConnectError> { ) -> Result<(), SimConnectError> {
unsafe { unsafe {
let (simconnect_period, simconnect_interval) = match period { let simconnect_period = match period {
PeriodEnum::VisualFrame { interval } => ( Period::Once => bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_ONCE,
bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_VISUAL_FRAME, Period::VisualFrame => bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_VISUAL_FRAME,
interval, Period::SimFrame => bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_SIM_FRAME,
),
PeriodEnum::Second => (bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_SECOND, 0), Period::Second => bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_SECOND,
}; };
let simconnect_flags: u32 = match condition { let simconnect_flags: u32 = match condition {
ConditionEnum::None => 0, Condition::None => 0,
ConditionEnum::Changed => bindings::SIMCONNECT_DATA_REQUEST_FLAG_CHANGED, Condition::Changed => bindings::SIMCONNECT_DATA_REQUEST_FLAG_CHANGED,
}; };
success!(bindings::SimConnect_RequestDataOnSimObject( success!(bindings::SimConnect_RequestDataOnSimObject(
@@ -146,7 +148,7 @@ impl SimConnect {
simconnect_period, simconnect_period,
simconnect_flags, simconnect_flags,
0, 0,
simconnect_interval, interval,
0, 0,
)); ));
} }

View File

@@ -1,5 +1,6 @@
use crate::{NotificationData, SimConnect, SimConnectError}; use crate::{NotificationData, SimConnect, SimConnectError};
/// Trait to be implemented by objects that can be registered with SimConnect.
pub trait SimConnectObjectExt: Clone + for<'a> TryFrom<&'a NotificationData> { pub trait SimConnectObjectExt: Clone + for<'a> TryFrom<&'a NotificationData> {
fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError>; fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError>;
} }