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
45const SLOT_COMPONENT_NAME: usize = 6;
47
48const SLOT_COMPONENT_VERSION: usize = 8;
50
51#[cfg(not(target_env = "msvc"))]
53type ComponentNameFn =
54 unsafe extern "C" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;
55
56#[cfg(target_env = "msvc")]
57type ComponentNameFn =
58 unsafe extern "thiscall" fn(*mut ServerComponent, *mut StringView) -> *mut StringView;
59
60#[cfg(not(target_env = "msvc"))]
62type ComponentVersionFn =
63 unsafe extern "C" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;
64
65#[cfg(target_env = "msvc")]
66type ComponentVersionFn =
67 unsafe extern "thiscall" fn(*mut ServerComponent, *mut SemanticVersion) -> *mut SemanticVersion;
68
69pub fn component_name<T: OmpComponentHandle>(c: &T) -> Option<String> {
75 let raw = c.as_raw().as_ptr();
76 let (_, slot) =
78 unsafe { super::vtable::secondary_call_target(raw.cast::<u8>(), 0, SLOT_COMPONENT_NAME)? };
79 let f: ComponentNameFn = unsafe { std::mem::transmute(slot) };
80 let mut sv = StringView {
81 data: std::ptr::null(),
82 len: 0,
83 };
84 unsafe { f(raw, &raw mut sv) };
85 if sv.data.is_null() || sv.len == 0 {
86 return None;
87 }
88 let bytes = unsafe { std::slice::from_raw_parts(sv.data, sv.len) };
89 std::str::from_utf8(bytes).ok().map(String::from)
90}
91
92pub fn component_version<T: OmpComponentHandle>(c: &T) -> Option<SemanticVersion> {
97 let raw = c.as_raw().as_ptr();
98 let (_, slot) = unsafe {
99 super::vtable::secondary_call_target(raw.cast::<u8>(), 0, SLOT_COMPONENT_VERSION)?
100 };
101 let f: ComponentVersionFn = unsafe { std::mem::transmute(slot) };
102 let mut version = SemanticVersion::new(0, 0, 0);
103 unsafe { f(raw, &raw mut version) };
104 Some(version)
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
116 use std::sync::Mutex;
117
118 static TEST_LOCK: Mutex<()> = Mutex::new(());
119
120 static MOCK_VTABLE: std::sync::OnceLock<[usize; 16]> = std::sync::OnceLock::new();
123
124 fn mock_vtable() -> &'static [usize; 16] {
125 MOCK_VTABLE.get_or_init(|| {
126 let mut v = [unused as *const () as usize; 16];
127 v[SLOT_COMPONENT_NAME] = mock_name as *const () as usize;
128 v[SLOT_COMPONENT_VERSION] = mock_version as *const () as usize;
129 v
130 })
131 }
132
133 #[cfg(not(target_env = "msvc"))]
140 unsafe extern "C" fn unused() {}
141 #[cfg(target_env = "msvc")]
142 unsafe extern "thiscall" fn unused() {}
143
144 static MOCK_NAME_BYTES: &[u8] = b"test-comp";
145
146 #[cfg(not(target_env = "msvc"))]
147 unsafe extern "C" fn mock_name(
148 _this: *mut ServerComponent,
149 out: *mut StringView,
150 ) -> *mut StringView {
151 unsafe {
152 *out = StringView {
153 data: MOCK_NAME_BYTES.as_ptr(),
154 len: MOCK_NAME_BYTES.len(),
155 };
156 }
157 out
158 }
159
160 #[cfg(target_env = "msvc")]
161 unsafe extern "thiscall" fn mock_name(
162 _this: *mut ServerComponent,
163 out: *mut StringView,
164 ) -> *mut StringView {
165 unsafe {
166 *out = StringView {
167 data: MOCK_NAME_BYTES.as_ptr(),
168 len: MOCK_NAME_BYTES.len(),
169 };
170 }
171 out
172 }
173
174 #[cfg(not(target_env = "msvc"))]
175 unsafe extern "C" fn mock_version(
176 _this: *mut ServerComponent,
177 out: *mut SemanticVersion,
178 ) -> *mut SemanticVersion {
179 unsafe {
180 *out = SemanticVersion::new(2, 7, 3);
181 }
182 out
183 }
184
185 #[cfg(target_env = "msvc")]
186 unsafe extern "thiscall" fn mock_version(
187 _this: *mut ServerComponent,
188 out: *mut SemanticVersion,
189 ) -> *mut SemanticVersion {
190 unsafe {
191 *out = SemanticVersion::new(2, 7, 3);
192 }
193 out
194 }
195
196 #[derive(Debug, Clone, Copy)]
198 struct DummyComponent {
199 ptr: NonNull<ServerComponent>,
200 }
201
202 impl OmpComponentHandle for DummyComponent {
203 const UID: UID = 0xDEAD_BEEF_CAFE_BABE;
204 unsafe fn from_raw(ptr: NonNull<ServerComponent>) -> Self {
205 Self { ptr }
206 }
207 fn as_raw(&self) -> NonNull<ServerComponent> {
208 self.ptr
209 }
210 }
211
212 fn make_mock_component() -> usize {
215 mock_vtable().as_ptr() as usize
216 }
217
218 #[test]
219 fn component_name_reads_slot_6_and_returns_string() {
220 let _g = TEST_LOCK.lock().unwrap();
221 let buf = make_mock_component();
222 let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
223 let nn = NonNull::new(raw).unwrap();
224 let comp = unsafe { DummyComponent::from_raw(nn) };
225
226 let name = component_name(&comp);
227 assert_eq!(name.as_deref(), Some("test-comp"));
228 }
229
230 #[test]
231 fn component_version_reads_slot_8_and_returns_semver() {
232 let _g = TEST_LOCK.lock().unwrap();
233 let buf = make_mock_component();
234 let raw = (&raw const buf).cast::<ServerComponent>().cast_mut();
235 let nn = NonNull::new(raw).unwrap();
236 let comp = unsafe { DummyComponent::from_raw(nn) };
237
238 let v = component_version(&comp).unwrap();
239 assert_eq!((v.major, v.minor, v.patch), (2, 7, 3));
240 }
241
242 #[test]
243 fn dummy_component_uid_is_consistent() {
244 assert_eq!(DummyComponent::UID, 0xDEAD_BEEF_CAFE_BABE);
245 }
246}