samp_sdk/omp/
component_api.rs1use super::server::ServerComponent;
17use super::types::{SemanticVersion, StringView, UID};
18use std::ptr::NonNull;
19
20pub trait OmpComponentHandle: Sized + Copy {
31 const UID: UID;
33
34 unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self;
40
41 fn as_raw(&self) -> NonNull<ServerComponent>;
43}
44
45#[cfg(not(target_env = "msvc"))]
51const SLOT_COMPONENT_NAME: usize = 7;
52
53#[cfg(target_env = "msvc")]
54const SLOT_COMPONENT_NAME: usize = 6;
55
56#[cfg(not(target_env = "msvc"))]
59const SLOT_COMPONENT_VERSION: usize = 9;
60
61#[cfg(target_env = "msvc")]
62const SLOT_COMPONENT_VERSION: usize = 8;
63
64#[cfg(not(target_env = "msvc"))]
67type ComponentNameFn = unsafe extern "C" fn(*mut ServerComponent) -> StringView;
68
69#[cfg(target_env = "msvc")]
70type ComponentNameFn =
71 unsafe extern "thiscall" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;
72
73#[cfg(not(target_env = "msvc"))]
76type ComponentVersionFn = unsafe extern "C" fn(*mut ServerComponent) -> SemanticVersion;
77
78#[cfg(target_env = "msvc")]
79type ComponentVersionFn =
80 unsafe extern "thiscall" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;
81
82pub fn component_name<T: OmpComponentHandle>(c: &T) -> Option<String> {
88 let raw = c.as_raw().as_ptr();
89 let (_, slot) = unsafe {
91 super::vtable::secondary_call_target_ptr(raw.cast::<u8>(), 0, SLOT_COMPONENT_NAME)?
92 };
93 let f: ComponentNameFn = unsafe { std::mem::transmute(slot) };
94
95 #[cfg(not(target_env = "msvc"))]
100 let sv = unsafe { f(raw) };
101
102 #[cfg(target_env = "msvc")]
103 let sv = {
104 let mut sv = StringView {
105 data: std::ptr::null(),
106 len: 0,
107 };
108 unsafe { f(raw, &raw mut sv) };
109 sv
110 };
111 if sv.data.is_null() || sv.len == 0 {
112 return None;
113 }
114 let bytes = unsafe { std::slice::from_raw_parts(sv.data, sv.len) };
115 std::str::from_utf8(bytes).ok().map(String::from)
116}
117
118pub fn component_version<T: OmpComponentHandle>(c: &T) -> Option<SemanticVersion> {
123 let raw = c.as_raw().as_ptr();
124 let (_, slot) = unsafe {
125 super::vtable::secondary_call_target_ptr(raw.cast::<u8>(), 0, SLOT_COMPONENT_VERSION)?
126 };
127 let f: ComponentVersionFn = unsafe { std::mem::transmute(slot) };
128
129 #[cfg(not(target_env = "msvc"))]
130 let version = unsafe { f(raw) };
131
132 #[cfg(target_env = "msvc")]
133 let version = {
134 let mut version = SemanticVersion::new(0, 0, 0);
135 unsafe { f(raw, &raw mut version) };
136 version
137 };
138 Some(version)
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
150 use crate::omp::vtable::MockTable;
151 use std::sync::Mutex;
152
153 static TEST_LOCK: Mutex<()> = Mutex::new(());
154
155 static MOCK_VTABLE: std::sync::OnceLock<MockTable<16>> = std::sync::OnceLock::new();
158
159 fn mock_vtable() -> &'static [*const (); 16] {
160 &MOCK_VTABLE
161 .get_or_init(|| {
162 let mut v = [unused as *const (); 16];
163 v[SLOT_COMPONENT_NAME] = mock_name as *const ();
164 v[SLOT_COMPONENT_VERSION] = mock_version as *const ();
165 MockTable(v)
166 })
167 .0
168 }
169
170 #[cfg(not(target_env = "msvc"))]
177 unsafe extern "C" fn unused() {}
178 #[cfg(target_env = "msvc")]
179 unsafe extern "thiscall" fn unused() {}
180
181 static MOCK_NAME_BYTES: &[u8] = b"test-comp";
182
183 #[cfg(not(target_env = "msvc"))]
184 unsafe extern "C" fn mock_name(_this: *mut ServerComponent) -> StringView {
186 StringView {
187 data: MOCK_NAME_BYTES.as_ptr(),
188 len: MOCK_NAME_BYTES.len(),
189 }
190 }
191
192 #[cfg(target_env = "msvc")]
193 unsafe extern "thiscall" fn mock_name(
194 _this: *mut ServerComponent,
195 out: *mut StringView,
196 ) -> *mut StringView {
197 unsafe {
198 *out = StringView {
199 data: MOCK_NAME_BYTES.as_ptr(),
200 len: MOCK_NAME_BYTES.len(),
201 };
202 }
203 out
204 }
205
206 #[cfg(not(target_env = "msvc"))]
207 unsafe extern "C" fn mock_version(_this: *mut ServerComponent) -> SemanticVersion {
208 SemanticVersion::new(2, 7, 3)
209 }
210
211 #[cfg(target_env = "msvc")]
212 unsafe extern "thiscall" fn mock_version(
213 _this: *mut ServerComponent,
214 out: *mut SemanticVersion,
215 ) -> *mut SemanticVersion {
216 unsafe {
217 *out = SemanticVersion::new(2, 7, 3);
218 }
219 out
220 }
221
222 #[derive(Debug, Clone, Copy)]
224 struct DummyComponent {
225 ptr: NonNull<ServerComponent>,
226 }
227
228 impl OmpComponentHandle for DummyComponent {
229 const UID: UID = 0xDEAD_BEEF_CAFE_BABE;
230 unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
231 Self { ptr }
232 }
233 fn as_raw(&self) -> NonNull<ServerComponent> {
234 self.ptr
235 }
236 }
237
238 fn make_mock_component() -> *const *const () {
241 mock_vtable().as_ptr()
242 }
243
244 #[test]
247 fn component_slots_match_the_official_binaries() {
248 #[cfg(not(target_env = "msvc"))]
249 {
250 assert_eq!(SLOT_COMPONENT_NAME, 7);
251 assert_eq!(SLOT_COMPONENT_VERSION, 9);
252 }
253 #[cfg(target_env = "msvc")]
254 {
255 assert_eq!(SLOT_COMPONENT_NAME, 6);
256 assert_eq!(SLOT_COMPONENT_VERSION, 8);
257 }
258 }
259
260 #[test]
261 fn component_name_reads_slot_6_and_returns_string() {
262 let _g = TEST_LOCK.lock().unwrap();
263 let buf = make_mock_component();
264 let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
265 let nn = NonNull::new(raw).unwrap();
266 let comp = unsafe { DummyComponent::from_raw(nn) };
267
268 let name = component_name(&comp);
269 assert_eq!(name.as_deref(), Some("test-comp"));
270 }
271
272 #[test]
273 fn component_version_reads_slot_8_and_returns_semver() {
274 let _g = TEST_LOCK.lock().unwrap();
275 let buf = make_mock_component();
276 let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
277 let nn = NonNull::new(raw).unwrap();
278 let comp = unsafe { DummyComponent::from_raw(nn) };
279
280 let v = component_version(&comp).unwrap();
281 assert_eq!((v.major, v.minor, v.patch), (2, 7, 3));
282 }
283
284 #[test]
285 fn dummy_component_uid_is_consistent() {
286 assert_eq!(DummyComponent::UID, 0xDEAD_BEEF_CAFE_BABE);
287 }
288}