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}