From 1cf6b2d94e3b252b412213eb8d9cd1f1902f8613 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 30 Jul 2023 11:49:29 +0200 Subject: [PATCH] godot-ffi: move symbols into `toolbox` + `gdextension_plus` modules --- ReadMe.md | 3 +- godot-ffi/src/gdextension_plus.rs | 110 +++++++++++++ godot-ffi/src/lib.rs | 255 +----------------------------- godot-ffi/src/toolbox.rs | 167 +++++++++++++++++++ 4 files changed, 285 insertions(+), 250 deletions(-) create mode 100644 godot-ffi/src/gdextension_plus.rs create mode 100644 godot-ffi/src/toolbox.rs diff --git a/ReadMe.md b/ReadMe.md index dc4699f43..293c08e1b 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -29,7 +29,7 @@ However, it is still in an early stage and there are certain things to keep in m > **Warning** > The public API introduces breaking changes from time to time. Most of these are motivated by new features and -> improved ergonomics for existing ones. Once we are on crates.io, we will adhere to SemVer for releases. +> improved ergonomics for existing ones. See also [API stability] in the book. **Features:** While most Godot features are available, some less commonly used ones are missing. See [#24] for an up-to-date overview. At this point, there is **no** support for Android, iOS or WASM. Contributions are very welcome! @@ -67,6 +67,7 @@ Contributions are very welcome! If you want to help out, see [`Contributing.md`] [#24]: https://github.com/godot-rust/gdext/issues/24 [`gdnative`]: https://github.com/godot-rust/gdnative [API Docs]: https://godot-rust.github.io/docs/gdext +[API stability]: https://godot-rust.github.io/book/gdext/advanced/compatibility.html#rust-api-stability [book]: https://godot-rust.github.io/book/gdext [Discord]: https://discord.gg/aKUCJ8rJsc [dodge-the-creeps]: examples/dodge-the-creeps diff --git a/godot-ffi/src/gdextension_plus.rs b/godot-ffi/src/gdextension_plus.rs new file mode 100644 index 000000000..ba48fc355 --- /dev/null +++ b/godot-ffi/src/gdextension_plus.rs @@ -0,0 +1,110 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Extra functionality to enrich low-level C API. + +use crate::gen::gdextension_interface::*; +use crate::VariantType; + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Static checks + +// The impls only compile if those are different types -- ensures type safety through patch +trait Distinct {} +impl Distinct for GDExtensionVariantPtr {} +impl Distinct for GDExtensionTypePtr {} +impl Distinct for GDExtensionConstTypePtr {} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Extension traits for conversion + +/// Convert a GDExtension pointer type to its uninitialized version. +pub trait AsUninit { + type Ptr; + + #[allow(clippy::wrong_self_convention)] + fn as_uninit(self) -> Self::Ptr; + + fn force_init(uninit: Self::Ptr) -> Self; +} + +macro_rules! impl_as_uninit { + ($Ptr:ty, $Uninit:ty) => { + impl AsUninit for $Ptr { + type Ptr = $Uninit; + + fn as_uninit(self) -> $Uninit { + self as $Uninit + } + + fn force_init(uninit: Self::Ptr) -> Self { + uninit as Self + } + } + }; +} + +#[rustfmt::skip] +impl_as_uninit!(GDExtensionStringNamePtr, GDExtensionUninitializedStringNamePtr); +impl_as_uninit!(GDExtensionVariantPtr, GDExtensionUninitializedVariantPtr); +impl_as_uninit!(GDExtensionStringPtr, GDExtensionUninitializedStringPtr); +impl_as_uninit!(GDExtensionObjectPtr, GDExtensionUninitializedObjectPtr); +impl_as_uninit!(GDExtensionTypePtr, GDExtensionUninitializedTypePtr); + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Helper functions + +#[doc(hidden)] +#[inline] +pub fn default_call_error() -> GDExtensionCallError { + GDExtensionCallError { + error: GDEXTENSION_CALL_OK, + argument: -1, + expected: -1, + } +} + +#[doc(hidden)] +#[inline] +#[track_caller] // panic message points to call site +pub fn panic_call_error( + err: &GDExtensionCallError, + function_name: &str, + arg_types: &[VariantType], +) -> ! { + debug_assert_ne!(err.error, GDEXTENSION_CALL_OK); // already checked outside + + let GDExtensionCallError { + error, + argument, + expected, + } = *err; + + let argc = arg_types.len(); + let reason = match error { + GDEXTENSION_CALL_ERROR_INVALID_METHOD => "method not found".to_string(), + GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => { + let from = arg_types[argument as usize]; + let to = VariantType::from_sys(expected as GDExtensionVariantType); + let i = argument + 1; + + format!("cannot convert argument #{i} from {from:?} to {to:?}") + } + GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS => { + format!("too many arguments; expected {argument}, but called with {argc}") + } + GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => { + format!("too few arguments; expected {argument}, but called with {argc}") + } + GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "instance is null".to_string(), + GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "method is not const".to_string(), // not handled in Godot + _ => format!("unknown reason (error code {error})"), + }; + + // Note: Godot also outputs thread ID + // In Godot source: variant.cpp:3043 or core_bind.cpp:2742 + panic!("Function call failed: {function_name} -- {reason}."); +} diff --git a/godot-ffi/src/lib.rs b/godot-ffi/src/lib.rs index ecea95edf..174f876e9 100644 --- a/godot-ffi/src/lib.rs +++ b/godot-ffi/src/lib.rs @@ -20,9 +20,11 @@ pub(crate) mod gen; mod compat; +mod gdextension_plus; mod godot_ffi; mod opaque; mod plugins; +mod toolbox; use compat::BindingCompat; use std::ffi::CStr; @@ -36,36 +38,14 @@ pub use crate::godot_ffi::{ from_sys_init_or_init_default, GodotFfi, GodotFuncMarshal, GodotNullablePtr, PrimitiveConversionError, PtrcallType, }; +pub use gdextension_plus::*; pub use gen::central::*; pub use gen::gdextension_interface::*; pub use gen::interface::*; - -// The impls only compile if those are different types -- ensures type safety through patch -trait Distinct {} -impl Distinct for GDExtensionVariantPtr {} -impl Distinct for GDExtensionTypePtr {} -impl Distinct for GDExtensionConstTypePtr {} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -#[cfg(feature = "trace")] -#[macro_export] -macro_rules! out { - () => (eprintln!()); - ($fmt:literal) => (eprintln!($fmt)); - ($fmt:literal, $($arg:tt)*) => (eprintln!($fmt, $($arg)*)); -} - -#[cfg(not(feature = "trace"))] -// TODO find a better way than sink-writing to avoid warnings, #[allow(unused_variables)] doesn't work -#[macro_export] -macro_rules! out { - () => ({}); - ($fmt:literal) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt); }); - ($fmt:literal, $($arg:tt)*) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt, $($arg)*); };) -} +pub use toolbox::*; // ---------------------------------------------------------------------------------------------------------------------------------------------- +// API to access Godot via FFI struct GodotBinding { interface: GDExtensionInterface, @@ -170,28 +150,8 @@ pub(crate) unsafe fn runtime_metadata() -> &'static GdextRuntimeMetadata { &BINDING.as_ref().unwrap().runtime_metadata } -/// Makes sure that Godot is running, or panics. Debug mode only! -macro_rules! debug_assert_godot { - ($expr:expr) => { - debug_assert!( - $expr, - "Godot engine not available; make sure you are not calling it from unit/doc tests" - ); // previous message: "unchecked access to Option::None" - }; -} - -/// Combination of `as_ref()` and `unwrap_unchecked()`, but without the case differentiation in -/// the former (thus raw pointer access in release mode) -unsafe fn unwrap_ref_unchecked(opt: &Option) -> &T { - debug_assert_godot!(opt.is_some()); - - match opt { - Some(ref val) => val, - None => std::hint::unreachable_unchecked(), - } -} - // ---------------------------------------------------------------------------------------------------------------------------------------------- +// Macros to access low-level function bindings #[macro_export] #[doc(hidden)] @@ -216,206 +176,3 @@ macro_rules! interface_fn { unsafe { $crate::get_interface().$name.unwrap_unchecked() } }}; } - -/// Verifies a condition at compile time. -// https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html#panic-in-const-contexts -#[macro_export] -macro_rules! static_assert { - ($cond:expr) => { - const _: () = assert!($cond); - }; - ($cond:expr, $msg:literal) => { - const _: () = assert!($cond, $msg); - }; -} - -/// Verifies at compile time that two types `T` and `U` have the same size. -#[macro_export] -macro_rules! static_assert_eq_size { - ($T:ty, $U:ty) => { - godot_ffi::static_assert!(std::mem::size_of::<$T>() == std::mem::size_of::<$U>()); - }; - ($T:ty, $U:ty, $msg:literal) => { - godot_ffi::static_assert!(std::mem::size_of::<$T>() == std::mem::size_of::<$U>(), $msg); - }; -} - -/// Extract value from box before `into_inner()` is stable -#[allow(clippy::boxed_local)] // false positive -pub fn unbox(value: Box) -> T { - // Deref-move is a Box magic feature; see https://stackoverflow.com/a/42264074 - *value -} - -/// Explicitly cast away `const` from a pointer, similar to C++ `const_cast`. -/// -/// The `as` conversion simultaneously doing 10 other things, potentially causing unintended transmutations. -pub fn force_mut_ptr(ptr: *const T) -> *mut T { - ptr as *mut T -} - -/// Add `const` to a mut ptr. -pub fn to_const_ptr(ptr: *mut T) -> *const T { - ptr as *const T -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -/// Convert a GDExtension pointer type to its uninitialized version. -pub trait AsUninit { - type Ptr; - - #[allow(clippy::wrong_self_convention)] - fn as_uninit(self) -> Self::Ptr; - - fn force_init(uninit: Self::Ptr) -> Self; -} - -macro_rules! impl_as_uninit { - ($Ptr:ty, $Uninit:ty) => { - impl AsUninit for $Ptr { - type Ptr = $Uninit; - - fn as_uninit(self) -> $Uninit { - self as $Uninit - } - - fn force_init(uninit: Self::Ptr) -> Self { - uninit as Self - } - } - }; -} - -#[rustfmt::skip] -impl_as_uninit!(GDExtensionStringNamePtr, GDExtensionUninitializedStringNamePtr); -impl_as_uninit!(GDExtensionVariantPtr, GDExtensionUninitializedVariantPtr); -impl_as_uninit!(GDExtensionStringPtr, GDExtensionUninitializedStringPtr); -impl_as_uninit!(GDExtensionObjectPtr, GDExtensionUninitializedObjectPtr); -impl_as_uninit!(GDExtensionTypePtr, GDExtensionUninitializedTypePtr); - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -/// Metafunction to extract inner function pointer types from all the bindgen Option type names. -pub(crate) trait Inner: Sized { - type FnPtr: Sized; - - fn extract(self, error_msg: &str) -> Self::FnPtr; -} - -impl Inner for Option { - type FnPtr = T; - - fn extract(self, error_msg: &str) -> Self::FnPtr { - self.expect(error_msg) - } -} - -/// Extract a function pointer from its `Option` and convert it to the (dereferenced) target type. -/// -/// ```ignore -/// let get_godot_version = get_proc_address(sys::c_str(b"get_godot_version\0")); -/// let get_godot_version = sys::cast_fn_ptr!(get_godot_version as sys::GDExtensionInterfaceGetGodotVersion); -/// ``` -#[allow(unused)] -#[macro_export] -macro_rules! cast_fn_ptr { - ($option:ident as $ToType:ty) => {{ - let ptr = $option.expect("null function pointer"); - std::mem::transmute::::FnPtr>(ptr) - }}; -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -/// If `ptr` is not null, returns `Some(mapper(ptr))`; otherwise `None`. -#[inline] -pub fn ptr_then(ptr: *mut T, mapper: F) -> Option -where - F: FnOnce(*mut T) -> R, -{ - // Could also use NonNull in signature, but for this project we always deal with FFI raw pointers - if ptr.is_null() { - None - } else { - Some(mapper(ptr)) - } -} - -/// Returns a C `const char*` for a null-terminated byte string. -#[inline] -pub fn c_str(s: &[u8]) -> *const std::ffi::c_char { - // Ensure null-terminated - debug_assert!(!s.is_empty() && s[s.len() - 1] == 0); - - s.as_ptr() as *const std::ffi::c_char -} - -#[inline] -pub fn c_str_from_str(s: &str) -> *const std::ffi::c_char { - debug_assert!(s.is_ascii()); - - c_str(s.as_bytes()) -} - -/// Returns an ad-hoc hash of any object. -pub fn hash_value(t: &T) -> u64 { - use std::hash::Hasher; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - t.hash(&mut hasher); - hasher.finish() -} - -// ---------------------------------------------------------------------------------------------------------------------------------------------- - -#[doc(hidden)] -#[inline] -pub fn default_call_error() -> GDExtensionCallError { - GDExtensionCallError { - error: GDEXTENSION_CALL_OK, - argument: -1, - expected: -1, - } -} - -#[doc(hidden)] -#[inline] -#[track_caller] // panic message points to call site -pub fn panic_call_error( - err: &GDExtensionCallError, - function_name: &str, - arg_types: &[VariantType], -) -> ! { - debug_assert_ne!(err.error, GDEXTENSION_CALL_OK); // already checked outside - - let GDExtensionCallError { - error, - argument, - expected, - } = *err; - - let argc = arg_types.len(); - let reason = match error { - GDEXTENSION_CALL_ERROR_INVALID_METHOD => "method not found".to_string(), - GDEXTENSION_CALL_ERROR_INVALID_ARGUMENT => { - let from = arg_types[argument as usize]; - let to = VariantType::from_sys(expected as GDExtensionVariantType); - let i = argument + 1; - - format!("cannot convert argument #{i} from {from:?} to {to:?}") - } - GDEXTENSION_CALL_ERROR_TOO_MANY_ARGUMENTS => { - format!("too many arguments; expected {argument}, but called with {argc}") - } - GDEXTENSION_CALL_ERROR_TOO_FEW_ARGUMENTS => { - format!("too few arguments; expected {argument}, but called with {argc}") - } - GDEXTENSION_CALL_ERROR_INSTANCE_IS_NULL => "instance is null".to_string(), - GDEXTENSION_CALL_ERROR_METHOD_NOT_CONST => "method is not const".to_string(), // not handled in Godot - _ => format!("unknown reason (error code {error})"), - }; - - // Note: Godot also outputs thread ID - // In Godot source: variant.cpp:3043 or core_bind.cpp:2742 - panic!("Function call failed: {function_name} -- {reason}."); -} diff --git a/godot-ffi/src/toolbox.rs b/godot-ffi/src/toolbox.rs new file mode 100644 index 000000000..34b6fca0a --- /dev/null +++ b/godot-ffi/src/toolbox.rs @@ -0,0 +1,167 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Functions and macros that are not very specific to gdext, but come in handy. + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Macros + +/// Verifies a condition at compile time. +// https://blog.rust-lang.org/2021/12/02/Rust-1.57.0.html#panic-in-const-contexts +#[macro_export] +macro_rules! static_assert { + ($cond:expr) => { + const _: () = assert!($cond); + }; + ($cond:expr, $msg:literal) => { + const _: () = assert!($cond, $msg); + }; +} + +/// Verifies at compile time that two types `T` and `U` have the same size. +#[macro_export] +macro_rules! static_assert_eq_size { + ($T:ty, $U:ty) => { + godot_ffi::static_assert!(std::mem::size_of::<$T>() == std::mem::size_of::<$U>()); + }; + ($T:ty, $U:ty, $msg:literal) => { + godot_ffi::static_assert!(std::mem::size_of::<$T>() == std::mem::size_of::<$U>(), $msg); + }; +} + +/// Trace output. +#[cfg(feature = "trace")] +#[macro_export] +macro_rules! out { + () => (eprintln!()); + ($fmt:literal) => (eprintln!($fmt)); + ($fmt:literal, $($arg:tt)*) => (eprintln!($fmt, $($arg)*)); +} + +/// Trace output. +#[cfg(not(feature = "trace"))] +// TODO find a better way than sink-writing to avoid warnings, #[allow(unused_variables)] doesn't work +#[macro_export] +macro_rules! out { + () => ({}); + ($fmt:literal) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt); }); + ($fmt:literal, $($arg:tt)*) => ({ use std::io::{sink, Write}; let _ = write!(sink(), $fmt, $($arg)*); };) +} + +/// Extract a function pointer from its `Option` and convert it to the (dereferenced) target type. +/// +/// ```ignore +/// let get_godot_version = get_proc_address(sys::c_str(b"get_godot_version\0")); +/// let get_godot_version = sys::cast_fn_ptr!(get_godot_version as sys::GDExtensionInterfaceGetGodotVersion); +/// ``` +#[allow(unused)] +#[macro_export] +macro_rules! cast_fn_ptr { + ($option:ident as $ToType:ty) => {{ + let ptr = $option.expect("null function pointer"); + std::mem::transmute::::FnPtr>(ptr) + }}; +} + +/// Makes sure that Godot is running, or panics. Debug mode only! +/// (private macro) +macro_rules! debug_assert_godot { + ($expr:expr) => { + debug_assert!( + $expr, + "Godot engine not available; make sure you are not calling it from unit/doc tests" + ); // previous message: "unchecked access to Option::None" + }; +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Utility functions + +/// Combination of `as_ref()` and `unwrap_unchecked()`, but without the case differentiation in +/// the former (thus raw pointer access in release mode) +pub(crate) unsafe fn unwrap_ref_unchecked(opt: &Option) -> &T { + debug_assert_godot!(opt.is_some()); + + match opt { + Some(ref val) => val, + None => std::hint::unreachable_unchecked(), + } +} + +/// Extract value from box before `into_inner()` is stable +#[allow(clippy::boxed_local)] // false positive +pub fn unbox(value: Box) -> T { + // Deref-move is a Box magic feature; see https://stackoverflow.com/a/42264074 + *value +} + +/// Explicitly cast away `const` from a pointer, similar to C++ `const_cast`. +/// +/// The `as` conversion simultaneously doing 10 other things, potentially causing unintended transmutations. +pub fn force_mut_ptr(ptr: *const T) -> *mut T { + ptr as *mut T +} + +/// Add `const` to a mut ptr. +pub fn to_const_ptr(ptr: *mut T) -> *const T { + ptr as *const T +} +/// If `ptr` is not null, returns `Some(mapper(ptr))`; otherwise `None`. +#[inline] +pub fn ptr_then(ptr: *mut T, mapper: F) -> Option +where + F: FnOnce(*mut T) -> R, +{ + // Could also use NonNull in signature, but for this project we always deal with FFI raw pointers + if ptr.is_null() { + None + } else { + Some(mapper(ptr)) + } +} + +/// Returns a C `const char*` for a null-terminated byte string. +#[inline] +pub fn c_str(s: &[u8]) -> *const std::ffi::c_char { + // Ensure null-terminated + debug_assert!(!s.is_empty() && s[s.len() - 1] == 0); + + s.as_ptr() as *const std::ffi::c_char +} + +#[inline] +pub fn c_str_from_str(s: &str) -> *const std::ffi::c_char { + debug_assert!(s.is_ascii()); + + c_str(s.as_bytes()) +} + +/// Returns an ad-hoc hash of any object. +pub fn hash_value(t: &T) -> u64 { + use std::hash::Hasher; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + t.hash(&mut hasher); + hasher.finish() +} + +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// Private helpers + +/// Metafunction to extract inner function pointer types from all the bindgen Option type names. +/// Needed for `cast_fn_ptr` macro. +pub(crate) trait Inner: Sized { + type FnPtr: Sized; + + fn extract(self, error_msg: &str) -> Self::FnPtr; +} + +impl Inner for Option { + type FnPtr = T; + + fn extract(self, error_msg: &str) -> Self::FnPtr { + self.expect(error_msg) + } +}