gm_ffi/
lib.rs

1//! A Rust crate to interface between GameMaker and Rust.
2
3#![cfg_attr(test, allow(clippy::float_cmp))] // lets us compare floats in asserts
4#![deny(rust_2018_idioms)]
5#![deny(rustdoc::broken_intra_doc_links)]
6#![deny(missing_docs)]
7
8use core::ffi::c_char;
9
10/// A status code the represents the outcome of a Rust-side function,
11/// intended to be sent back to GameMaker.
12#[derive(Debug, Copy, Clone, PartialEq)]
13#[repr(transparent)]
14pub struct OutputCode(f64);
15
16impl OutputCode {
17    /// Represents an operation that executed as intended.
18    pub const SUCCESS: OutputCode = OutputCode(1.0);
19    /// Represents an operation that failed to execute as intended.
20    pub const FAILURE: OutputCode = OutputCode(0.0);
21
22    /// Creates a custom OutputCode. This can mean whatever you want it to mean,
23    /// for example, returning the number of bytes written into a shared buffer.
24    pub const fn custom(code: f64) -> Self {
25        Self(code)
26    }
27}
28
29// blanket implementation
30impl<T, E> From<Result<T, E>> for OutputCode {
31    fn from(o: Result<T, E>) -> Self {
32        if o.is_ok() {
33            OutputCode::SUCCESS
34        } else {
35            OutputCode::FAILURE
36        }
37    }
38}
39
40/// Representation of a pointer sent from GameMaker. Dereferences
41/// into its inner c_char.
42#[repr(transparent)]
43#[derive(Debug, Copy, Clone, PartialEq, Eq)]
44pub struct GmPtr(*const c_char);
45
46impl GmPtr {
47    /// Creates a new GmPtr based on the given pointer.
48    pub fn new(ptr: *const c_char) -> Self {
49        Self(ptr)
50    }
51
52    /// Returns a self with `NULL` inside it. Be careful out there!
53    ///
54    /// # Safety
55    /// It's a nullptr, come on you dummy! You can obviously break everything
56    /// with this.
57    pub const fn null() -> Self {
58        Self(core::ptr::null())
59    }
60
61    /// Returns a copy of the inner value.
62    pub const fn inner(self) -> *const c_char {
63        self.0
64    }
65
66    /// Transforms the inner value into an &str.
67    ///
68    /// # Saftey
69    /// Assumes that the pointer being used is valid as a c_str pointer.
70    pub fn to_str(self) -> Result<&'static str, core::str::Utf8Error> {
71        unsafe { core::ffi::CStr::from_ptr(self.0) }.to_str()
72    }
73}
74
75impl core::ops::Deref for GmPtr {
76    type Target = *const c_char;
77    fn deref(&self) -> &Self::Target {
78        &self.0
79    }
80}
81
82impl core::ops::DerefMut for GmPtr {
83    fn deref_mut(&mut self) -> &mut Self::Target {
84        &mut self.0
85    }
86}
87
88unsafe impl Send for GmPtr {}
89unsafe impl Sync for GmPtr {}
90
91/// This is a Gm Id for a buffer, or any other dynamically allocated resource.
92/// It is transparent in memory but opaque in type (ie, you can't inspect what's inside it),
93/// so it can be sent back and forth to GM as an f64.
94///
95/// If you want to inspect an ID from Gm, you probably want `GmResourceId`, which is transparent
96/// in type as well.
97///
98/// Generally, you shouldn't be constructing this, but should be getting this from Gm.
99/// The one exception is in Unit Tests, where you can get access to a `new` method, or
100/// the `dummy` variant, which will give you an f64::MAX inside.
101#[repr(transparent)]
102#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
103pub struct GmId(f64);
104
105impl GmId {
106    /// Creates a new ID. This is intended for Units Tests.
107    #[cfg(test)]
108    pub const fn new(id: f64) -> Self {
109        Self(id)
110    }
111
112    /// Returns a dummy, with the f64::MAX inside it.
113    pub const fn dummy() -> Self {
114        Self(f64::MAX)
115    }
116}
117
118/// This is a Gm Real, which can be a resource, or any other stable resource.
119///
120/// Generally, you shouldn't be constructing this, but should be getting this from Gm.
121/// The one exception is in Unit Tests, where you can get access to a `new` method, or
122/// the `dummy` variant, which will give you an f64::MAX inside.
123#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
124#[repr(transparent)]
125pub struct GmReal(pub f64);
126
127impl GmReal {
128    /// Creates a new ID. You probably shouldn't be using this, but there are times
129    /// when you might be reconstructing it.
130    pub const fn new(id: f64) -> Self {
131        Self(id)
132    }
133
134    /// Returns the inner as a usize.
135    pub const fn as_usize(self) -> usize {
136        self.0 as usize
137    }
138
139    /// Returns the inner as an f64.
140    pub const fn as_f64(self) -> f64 {
141        self.0
142    }
143
144    /// Returns the inner f64.
145    pub const fn inner(self) -> f64 {
146        self.0
147    }
148
149    /// Returns a dummy, with the f64::MAX inside it.
150    pub const fn dummy() -> Self {
151        Self(f64::MAX)
152    }
153}
154
155/// Our basic GmBuffer. This holds anything you want.
156///
157/// # Safety
158/// We LIE to Rust and tell it that the buffer held within is `'static`.
159/// It is **not** static, but we're going to act like it is. Because GM is our
160/// allocator, a user could easily decide to deallocate a buffer.
161///
162/// We would very much so like if they don't do that, and will pretend like they cannot.
163/// If, however, they do, this entire data structure will be inadequate.
164#[derive(Debug)]
165pub struct GmBuffer<T: 'static> {
166    /// An Id for the GameMaker buffer to return when we want to destruct this.
167    id: GmId,
168
169    /// The actual vertex buffer that we write to.
170    pub buffer: &'static mut [T],
171}
172
173impl<T> GmBuffer<T> {
174    /// Creates a new Gm Buffer.
175    ///
176    /// - `gm_id` is the id, in GameMaker, of the buffer we're trying to create.
177    /// - `gm_ptr` is the pointer provided to the buffer that GameMaker gives us.
178    /// - `len` is the number of T's that can be fit within the buffer, **not** the
179    /// number of bytes. For more information, see [from_raw_parts](core::slice::from_raw_parts_mut)
180    ///
181    /// # Safety
182    /// Buffer must be allocated BY GAMEMAKER, not by some Rust code. The following invariants, in particular
183    /// must be held in order for this type to be safe:
184    /// - The buffer must be valid until `GmBuffer` is dropped
185    /// - The buffer's `id` must be a valid `GmId` from GameMaker.
186    /// - T must be sized, non-zero sized, and **must be zeroable**. This means that an "all zeroes"
187    ///   representation of the buffer is valid.  
188    pub unsafe fn new(gm_id: GmId, gm_ptr: GmPtr, len: usize) -> Self {
189        let buffer = {
190            let buf = gm_ptr.inner() as *mut T;
191
192            core::slice::from_raw_parts_mut(buf, len)
193        };
194
195        Self { id: gm_id, buffer }
196    }
197
198    /// This destructs the Buffer, taking self, and returning the Id. Once we give up ownership
199    /// of the ID by exposing it, we assume that we cannot safely hold onto the buffer anymore (ie,
200    /// we assume that it will be destroyed), and therefore, this function takes `self`.
201    pub fn id(self) -> GmId {
202        self.id
203    }
204}
205
206impl<T> core::ops::Index<usize> for GmBuffer<T> {
207    type Output = T;
208
209    fn index(&self, index: usize) -> &Self::Output {
210        &self.buffer[index]
211    }
212}
213
214impl<T> core::ops::IndexMut<usize> for GmBuffer<T> {
215    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
216        &mut self.buffer[index]
217    }
218}
219
220/// A GmBuffer whose purpose is to pass return data from Rust to GM. Useful in situations
221/// where you need to return an [OutputCode], but still have return data that needs to be
222/// communicated.
223///
224/// ## Safety
225/// The backing buffer must be *at least* 256 elements in size. This is just because we want to be
226/// able to write a large, but not too large amount of things.
227///
228/// 256 elements, in bytes, is `core::mem::size_of::<u32>() * 256`, or 1 kilobyte.
229pub struct Bridge(GmBuffer<u32>);
230
231impl Bridge {
232    /// Creates a new [Bridge] based upon a [GmBuffer].
233    pub fn new(buf: GmBuffer<u32>) -> Self {
234        debug_assert!(
235            buf.buffer.len() >= 256,
236            "your backing buffer needs to be at least 256 bytes"
237        );
238
239        Self(buf)
240    }
241
242    /// Creates a new [BridgeWriter] for this [GmBridge].
243    pub fn writer(&mut self) -> BridgeWriter<'_> {
244        BridgeWriter::new(self)
245    }
246}
247
248/// A utility for writing into a Bridge. Maintains a cursor, only relevant for its own
249/// writes.
250pub struct BridgeWriter<'a>(&'a mut Bridge, usize);
251impl<'a> BridgeWriter<'a> {
252    fn new(bridge: &'a mut Bridge) -> Self {
253        Self(bridge, 0)
254    }
255
256    /// Writes a u32 into the bridge at the [BridgeWriter]'s current position.
257    pub fn write_u32(&mut self, value: u32) {
258        self.0 .0[self.1] = value;
259        self.1 += 1;
260    }
261
262    /// Writes a f32 into the bridge at the [BridgeWriter]'s current position.
263    pub fn write_f32(&mut self, value: f32) {
264        self.0 .0[self.1] = value.to_bits();
265        self.1 += 1;
266    }
267}
268
269/// This is exactly like `println`, but works within NPC Studio DLLs. It's not ideal, but it does the job!
270#[macro_export]
271macro_rules! gm_println {
272    ($($arg:tt)*) => {
273        #[cfg(not(target_os = "windows"))]
274        {
275            use std::io::Write;
276
277            let mut output = $crate::GmStdOut::stdout();
278            output.write_fmt(format_args!($($arg)*)).unwrap();
279            output.write_str("\n");
280        }
281
282        #[cfg(target_os = "windows")]
283        {
284            println!($($arg)*);
285        }
286    };
287}
288
289/// This is exactly like `print`, but works within NPC Studio DLLs. It's not ideal, but it does the job!
290#[macro_export]
291macro_rules! gm_print {
292    ($($arg:tt)*) => {
293        #[cfg(not(target_os = "windows"))]
294        {
295            use std::io::Write;
296            let mut output = $crate::GmStdOut::stdout();
297            output.write_fmt(format_args!($($arg)*)).unwrap();
298        }
299
300        #[cfg(target_os = "windows")]
301        {
302            print!($($arg)*);
303        }
304    };
305}
306
307#[cfg(target_os = "windows")]
308mod windows_stub_gm_std_out {
309    /// Names the DLL for easier debugging
310    pub fn setup_panic_hook(program_name: &'static str) {
311        let base_message = format!("panicked in `{}` at ", program_name);
312
313        std::panic::set_hook(Box::new(move |panic_info| {
314            print!("{}", base_message);
315
316            if let Some(message) = panic_info.payload().downcast_ref::<String>() {
317                print!("'{}', ", message);
318            } else if let Some(message) = panic_info.payload().downcast_ref::<&'static str>() {
319                print!("'{}', ", message);
320            }
321
322            if let Some(location) = panic_info.location() {
323                print!("{}", location);
324            }
325            println!();
326        }));
327    }
328}
329
330#[cfg(not(target_os = "windows"))]
331mod mac_os_gm_std_out {
332    use interprocess::local_socket::LocalSocketStream;
333    use once_cell::sync::Lazy;
334    use parking_lot::RwLock;
335    use std::io::Write;
336
337    /// This struct abstracts for our purposes to only `adam`. It's not very useful
338    /// to people outside NPC Studio (unless they also use `adam`), so it's kept internally.
339    #[derive(Debug)]
340    pub struct GmStdOut(LocalSocketStream);
341
342    static GM_STD_OUT: Lazy<RwLock<GmStdOut>> = Lazy::new(|| {
343        let socket_name =
344            std::env::var("ADAM_IPC_SOCKET").expect("could not find `ADAM_IPC_SOCKET`");
345
346        let socket_stream =
347            LocalSocketStream::connect(socket_name).expect("could not connect to socket name!");
348        RwLock::new(GmStdOut(socket_stream))
349    });
350
351    impl GmStdOut {
352        /// Gets a handle to stdout.
353        pub fn stdout() -> impl std::ops::DerefMut<Target = GmStdOut> {
354            GM_STD_OUT.write()
355        }
356
357        /// Tries to write a string out, handling errors by not handling them at all.
358        pub fn write_str(&mut self, input: &str) {
359            let Ok(()) = self.0.write_all(&(input.len() as u64).to_le_bytes()) else { return; };
360            let Ok(()) = self.0.write_all(input.as_bytes()) else { return };
361            let _ = self.0.flush();
362        }
363    }
364
365    impl std::io::Write for GmStdOut {
366        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
367            self.0.write(buf)
368        }
369
370        fn flush(&mut self) -> std::io::Result<()> {
371            self.0.flush()
372        }
373
374        fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> std::io::Result<()> {
375            // Create a shim which translates a Write to a fmt::Write and saves
376            // off I/O errors. instead of discarding them
377            struct Adapter<'a> {
378                inner: &'a mut GmStdOut,
379            }
380
381            impl std::fmt::Write for Adapter<'_> {
382                fn write_str(&mut self, s: &str) -> std::fmt::Result {
383                    self.inner.write_str(s);
384
385                    Ok(())
386                }
387            }
388
389            let mut output = Adapter { inner: self };
390            let _ = std::fmt::write(&mut output, fmt);
391
392            Ok(())
393        }
394    }
395
396    /// This sets up a fairly decent panic hook. Pass in the name for us to format to use to identify the DLL.
397    pub fn setup_panic_hook(project_name: &str) {
398        let base_message = format!("panicked in `{}` at ", project_name);
399
400        std::panic::set_hook(Box::new(move |panic_info| {
401            use std::fmt::Write;
402
403            let mut output = base_message.clone();
404
405            if let Some(message) = panic_info.payload().downcast_ref::<String>() {
406                write!(output, "'{}', ", message).unwrap();
407            } else if let Some(message) = panic_info.payload().downcast_ref::<&'static str>() {
408                write!(output, "'{}', ", message).unwrap();
409            }
410
411            if let Some(location) = panic_info.location() {
412                write!(output, "{}", location).unwrap();
413            }
414            output.push('\n');
415
416            GmStdOut::stdout().write_str(&output);
417
418            std::process::exit(1);
419        }));
420    }
421}
422
423#[cfg(target_os = "windows")]
424pub use windows_stub_gm_std_out::setup_panic_hook;
425
426#[cfg(not(target_os = "windows"))]
427pub use mac_os_gm_std_out::{setup_panic_hook, GmStdOut};
428
429#[cfg(test)]
430mod tests {
431
432    use super::*;
433
434    #[test]
435    fn make_string_ptr() {
436        GmPtr::new("Hello, world!\0".as_ptr() as *const c_char);
437    }
438
439    #[test]
440    fn read_string_ptr() {
441        let ptr = GmPtr::new("Hello, world!\0".as_ptr() as *const c_char);
442        let out = ptr.to_str().unwrap();
443        assert_eq!(out, "Hello, world!");
444    }
445
446    #[test]
447    fn bridge() {
448        let buf = vec![0u32; 256];
449        let gm_ptr = GmPtr::new(buf.as_ptr() as *const _);
450
451        let mut bridge = unsafe { Bridge::new(GmBuffer::new(GmId::new(0.0), gm_ptr, 256)) };
452
453        let mut writer = bridge.writer();
454        writer.write_u32(18);
455        writer.write_f32(4.2);
456
457        assert_eq!(buf[0], 18);
458        assert_eq!(f32::from_bits(buf[1]), 4.2);
459
460        let mut writer = bridge.writer();
461        writer.write_f32(44.3);
462        writer.write_f32(22.2);
463
464        assert_eq!(f32::from_bits(buf[0]), 44.3);
465        assert_eq!(f32::from_bits(buf[1]), 22.2);
466    }
467}