Skip to main content

scs_sdk_plugin_macros/
lib.rs

1//! Procedural exports for the SCS telemetry and input plugin ABIs.
2//!
3//! The generated functions are intentionally kept out of application crates:
4//! all raw pointers, calling-convention declarations, symbol attributes, and
5//! panic containment live in the framework boundary. A plugin author supplies
6//! only a normal Rust expression which constructs a [`TelemetryPlugin`]
7//! implementation.
8//!
9//! [`TelemetryPlugin`]: https://docs.rs/scs-sdk-plugin/latest/scs_sdk_plugin/trait.TelemetryPlugin.html
10
11#![deny(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
12#![cfg_attr(
13    not(test),
14    deny(
15        clippy::expect_used,
16        clippy::panic,
17        clippy::todo,
18        clippy::unimplemented,
19        clippy::unreachable,
20        clippy::unwrap_used
21    )
22)]
23
24use proc_macro::TokenStream;
25use quote::quote;
26use syn::{Expr, parse_macro_input};
27
28/// Generates the two symbols loaded by ETS2 or ATS for one plugin instance.
29///
30/// After the framework validates the SDK version, initialization pointer, and
31/// idle lifecycle state, the input is evaluated exactly once for that accepted
32/// initialization attempt. It may be a constructor, a struct literal, or any
33/// other expression whose value implements
34/// `scs_sdk_plugin::TelemetryPlugin`. The compiler enforces that trait bound
35/// when the generated factory coerces the value into the framework's plugin
36/// object; an expression returning another type is a compile-time error.
37///
38/// The expansion owns a process-lifetime runtime object and emits:
39///
40/// - `scs_telemetry_init`, using the SCS system calling convention;
41/// - `scs_telemetry_shutdown`, which drains registrations before dropping the
42///   active plugin value.
43///
44/// ABI functions and raw SDK pointers exist only inside the expansion and
45/// `scs-sdk-plugin`; the invoking application's source remains ordinary safe
46/// Rust.
47///
48/// Invoke this macro exactly once in a plugin `cdylib`. Both exported names are
49/// fixed by the SCS loader contract, so two invocations in one link unit would
50/// define the same process runtime and symbols twice.
51///
52/// # Compile fixtures
53///
54/// The example below remains ignored as a rustdoc test because this proc-macro
55/// crate cannot depend back on `scs-sdk-plugin` without creating a Cargo
56/// dependency cycle. The same consumer code is compiled independently in
57/// `scs-sdk-plugin/tests/fixtures/export-plugin/pass`. A sibling fixture omits
58/// the trait implementation and must fail with E0277. Windows and Linux fixture
59/// builds also inspect the finished dynamic export tables for both SCS symbols.
60///
61/// # Example
62///
63/// ```ignore
64/// #[derive(Default)]
65/// struct MyPlugin;
66///
67/// impl scs_sdk_plugin::TelemetryPlugin for MyPlugin {
68///     fn metadata(&self) -> scs_sdk_plugin::PluginMetadata {
69///         scs_sdk_plugin::PluginMetadata::new("My Plugin", env!("CARGO_PKG_VERSION"))
70///     }
71///
72///     fn compatibility(&self) -> scs_sdk_plugin::PluginCompatibility {
73///         use scs_sdk_plugin::sdk::{TelemetryApiVersion, game};
74///         use scs_sdk_plugin::{Game, GameCompatibility, PluginCompatibility};
75///
76///         const GAMES: &[GameCompatibility] = &[GameCompatibility::new(
77///             Game::EuroTruckSimulator2,
78///             game::ets2::V1_00,
79///         )];
80///         PluginCompatibility::new(TelemetryApiVersion::V1_00, GAMES)
81///     }
82///
83///     fn initialize(
84///         &mut self,
85///         _context: &mut scs_sdk_plugin::PluginContext<'_>,
86///     ) -> scs_sdk_plugin::PluginResult {
87///         Ok(())
88///     }
89/// }
90///
91/// scs_sdk_plugin::export_plugin!(MyPlugin::default());
92/// ```
93#[proc_macro]
94pub fn export_plugin(input: TokenStream) -> TokenStream {
95    let constructor = parse_macro_input!(input as Expr);
96
97    quote! {
98        // SCS stores callback context pointers beyond initialization, so the
99        // runtime's address must never move while the library is loaded. A
100        // process-lifetime static provides that stable root; individual event
101        // and channel contexts remain in Arc allocations owned by Runtime.
102        static __SCS_SDK_PLUGIN_RUNTIME: ::scs_sdk_plugin::__private::Runtime =
103            ::scs_sdk_plugin::__private::Runtime::new();
104
105        #[doc = "Initializes the telemetry plugin through the safe scs-sdk-plugin runtime."]
106        #[doc = ""]
107        #[doc = "# Safety"]
108        #[doc = ""]
109        #[doc = "The game must pass the live initialization structure matching `version`"]
110        #[doc = "and obey the SCS Telemetry SDK main-thread lifecycle contract."]
111        #[unsafe(no_mangle)]
112        pub unsafe extern "system" fn scs_telemetry_init(
113            version: ::scs_sdk_plugin::__private::ScsU32,
114            params: *const ::scs_sdk_plugin::__private::ScsTelemetryInitParams,
115        ) -> ::scs_sdk_plugin::__private::ScsResult {
116            // SAFETY: This function is the loader-facing ABI boundary. The
117            // caller contract above guarantees that `params` identifies the
118            // live SDK structure for `version` and that execution is the
119            // serialized game-main-thread initialization call. Runtime borrows
120            // foreign data only for this call and contains every Rust panic.
121            unsafe {
122                __SCS_SDK_PLUGIN_RUNTIME.initialize(version, params, || {
123                    // The coercion to Box<dyn TelemetryPlugin> inside Runtime's
124                    // factory parameter is deliberate: it makes a constructor
125                    // returning the wrong type fail during compilation instead
126                    // of reaching an ABI entry point at runtime.
127                    ::std::boxed::Box::new(#constructor)
128                })
129            }
130        }
131
132        #[doc = "Stops the active telemetry plugin and releases SDK registrations."]
133        #[doc = ""]
134        #[doc = "SCS calls this entry point during its serialized shutdown lifecycle."]
135        #[doc = "The framework invokes the product shutdown hook, unregisters callbacks"]
136        #[doc = "in reverse order, and retains any context whose SDK unregistration fails."]
137        #[unsafe(no_mangle)]
138        pub extern "system" fn scs_telemetry_shutdown() {
139            __SCS_SDK_PLUGIN_RUNTIME.shutdown();
140        }
141    }
142    .into()
143}
144
145/// Generates the two loader-visible symbols for one SCS input plugin.
146///
147/// The constructor expression is evaluated once for each accepted input
148/// initialization attempt and must produce a value implementing
149/// `scs_sdk_plugin::InputPlugin`. The expansion owns an independent input
150/// runtime and emits exactly `scs_input_init` and `scs_input_shutdown`.
151///
152/// This macro may coexist with `export_plugin` in one dynamic library because
153/// the input and telemetry runtimes and exported symbol names are distinct.
154/// Application source remains safe; raw pointers and ABI declarations are
155/// generated only at the audited framework boundary.
156#[proc_macro]
157pub fn export_input_plugin(input: TokenStream) -> TokenStream {
158    let constructor = parse_macro_input!(input as Expr);
159
160    quote! {
161        // Input callbacks retain the opaque device context after initialization.
162        // The runtime therefore needs a stable process-lifetime root, separate
163        // from the telemetry runtime used by export_plugin!.
164        static __SCS_SDK_INPUT_PLUGIN_RUNTIME: ::scs_sdk_plugin::__private::InputRuntime =
165            ::scs_sdk_plugin::__private::InputRuntime::new();
166
167        #[doc = "Initializes the input plugin through the safe scs-sdk-plugin runtime."]
168        #[doc = ""]
169        #[doc = "# Safety"]
170        #[doc = ""]
171        #[doc = "The game must pass the live input initialization structure matching"]
172        #[doc = "version and obey the SCS Input SDK main-thread lifecycle contract."]
173        #[unsafe(no_mangle)]
174        pub unsafe extern "system" fn scs_input_init(
175            version: ::scs_sdk_plugin::__private::ScsU32,
176            params: *const ::scs_sdk_plugin::__private::ScsInputInitParams,
177        ) -> ::scs_sdk_plugin::__private::ScsResult {
178            // SAFETY: This is the loader-facing input ABI boundary. The caller
179            // provides the matching live initialization layout and serialized
180            // main-thread invocation. InputRuntime contains every Rust panic
181            // and keeps registered callback contexts at stable addresses.
182            unsafe {
183                __SCS_SDK_INPUT_PLUGIN_RUNTIME.initialize(version, params, || {
184                    // The expected Box<dyn InputPlugin> return type makes an
185                    // invalid constructor fail during compilation.
186                    ::std::boxed::Box::new(#constructor)
187                })
188            }
189        }
190
191        #[doc = "Stops the active input plugin after SCS unregisters its devices."]
192        #[doc = ""]
193        #[doc = "The framework invokes the product shutdown hook and releases the"]
194        #[doc = "successful generation's callback contexts."]
195        #[unsafe(no_mangle)]
196        pub extern "system" fn scs_input_shutdown() {
197            __SCS_SDK_INPUT_PLUGIN_RUNTIME.shutdown();
198        }
199    }
200    .into()
201}