diff --git a/godot-codegen/src/api_parser.rs b/godot-codegen/src/api_parser.rs index d76d19093..d8a3b8f50 100644 --- a/godot-codegen/src/api_parser.rs +++ b/godot-codegen/src/api_parser.rs @@ -21,6 +21,7 @@ pub struct ExtensionApi { pub classes: Vec, pub global_enums: Vec, pub utility_functions: Vec, + pub native_structures: Vec, pub singletons: Vec, } @@ -74,6 +75,12 @@ pub struct Class { // pub signals: Option>, } +#[derive(DeJson)] +pub struct NativeStructure { + pub name: String, + pub format: String, +} + #[derive(DeJson)] pub struct Singleton { pub name: String, diff --git a/godot-codegen/src/central_generator.rs b/godot-codegen/src/central_generator.rs index 8f13e3c64..7c1373577 100644 --- a/godot-codegen/src/central_generator.rs +++ b/godot-codegen/src/central_generator.rs @@ -82,6 +82,7 @@ pub(crate) fn generate_core_mod_file(core_gen_path: &Path, out_files: &mut Vec

, +) { + let _ = std::fs::remove_dir_all(gen_path); + std::fs::create_dir_all(gen_path).expect("create native directory"); + + let mut modules = vec![]; + for native_structure in api.native_structures.iter() { + let module_name = ModName::from_godot(&native_structure.name); + let class_name = TyName::from_godot(&native_structure.name); + + let generated_class = make_native_structure(native_structure, &class_name, ctx); + let file_contents = generated_class.code.to_string(); + + let out_path = gen_path.join(format!("{}.rs", module_name.rust_mod)); + std::fs::write(&out_path, file_contents).expect("failed to write native structures file"); + out_files.push(out_path); + + modules.push(GeneratedBuiltinModule { + class_name, + module_name, + }); + } + + let out_path = gen_path.join("mod.rs"); + let mod_contents = make_builtin_module_file(modules).to_string(); + std::fs::write(&out_path, mod_contents).expect("failed to write mod.rs file"); + out_files.push(out_path); +} + fn make_class_doc( class_name: &TyName, base_ident_opt: Option, @@ -296,8 +333,10 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate use godot_ffi as sys; use crate::engine::notify::*; use crate::builtin::*; + use crate::native_structure::*; use crate::obj::{AsArg, Gd}; use sys::GodotFfi as _; + use std::ffi::c_void; pub(super) mod re_export { use super::*; @@ -525,6 +564,7 @@ fn make_builtin_class( let tokens = quote! { use godot_ffi as sys; use crate::builtin::*; + use crate::native_structure::*; use crate::obj::{AsArg, Gd}; use crate::sys::GodotFfi as _; use crate::engine::Object; @@ -552,6 +592,69 @@ fn make_builtin_class( GeneratedBuiltin { code: tokens } } +fn make_native_structure( + structure: &NativeStructure, + class_name: &TyName, + ctx: &mut Context, +) -> GeneratedBuiltin { + let class_name = &class_name.rust_ty; + + let fields = make_native_structure_fields(&structure.format, ctx); + + // mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub + let tokens = quote! { + use godot_ffi as sys; + use crate::builtin::*; + use crate::native_structure::*; + use crate::obj::{AsArg, Gd}; + use crate::sys::GodotFfi as _; + use crate::engine::Object; + + #[repr(C)] + pub struct #class_name { + #fields + } + }; + // note: TypePtr -> ObjectPtr conversion OK? + + GeneratedBuiltin { code: tokens } +} + +fn make_native_structure_fields(format_str: &str, ctx: &mut Context) -> TokenStream { + let fields = parse_native_structures_format(format_str) + .expect("Could not parse native_structures format field"); + let field_definitions = fields + .into_iter() + .map(|field| make_native_structure_field_definition(field, ctx)); + quote! { + #( #field_definitions )* + } +} + +fn make_native_structure_field_definition( + field: NativeStructuresField, + ctx: &mut Context, +) -> TokenStream { + let field_type = normalize_native_structure_field_type(&field.field_type); + let field_type = to_rust_type_abi(&field_type, ctx); + let field_name = ident(&to_snake_case(&field.field_name)); + quote! { + pub #field_name: #field_type, + } +} + +fn normalize_native_structure_field_type(field_type: &str) -> String { + // native_structures uses a different format for enums than the + // rest of the JSON file. If we detect a scoped field, convert it + // to the enum format expected by to_rust_type. + if field_type.contains("::") { + let with_dot = field_type.replace("::", "."); + format!("enum::{}", with_dot) + } else { + field_type.to_string() + } +} + fn make_module_file(classes_and_modules: Vec) -> TokenStream { let mut class_decls = Vec::new(); let mut notify_decls = Vec::new(); @@ -718,20 +821,26 @@ fn make_special_builtin_methods(class_name: &TyName, _ctx: &Context) -> TokenStr #[cfg(not(feature = "codegen-full"))] fn is_type_excluded(ty: &str, ctx: &mut Context) -> bool { - let is_class_excluded = |class: &str| !crate::SELECTED_CLASSES.contains(&class); - - match to_rust_type(ty, ctx) { - RustTy::BuiltinIdent(_) => false, - RustTy::BuiltinArray(_) => false, - RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), - RustTy::EngineEnum { - surrounding_class, .. - } => match surrounding_class.as_ref() { - None => false, - Some(class) => is_class_excluded(class.as_str()), - }, - RustTy::EngineClass { .. } => is_class_excluded(ty), + fn is_class_excluded(class: &str) -> bool { + !crate::SELECTED_CLASSES.contains(&class) + } + + fn is_rust_type_excluded(ty: &RustTy) -> bool { + match ty { + RustTy::BuiltinIdent(_) => false, + RustTy::BuiltinArray(_) => false, + RustTy::RawPointer { inner, .. } => is_rust_type_excluded(&inner), + RustTy::EngineArray { elem_class, .. } => is_class_excluded(elem_class.as_str()), + RustTy::EngineEnum { + surrounding_class, .. + } => match surrounding_class.as_ref() { + None => false, + Some(class) => is_class_excluded(class.as_str()), + }, + RustTy::EngineClass { class, .. } => is_class_excluded(&class), + } } + is_rust_type_excluded(&to_rust_type(ty, ctx)) } fn is_method_excluded( @@ -743,11 +852,6 @@ fn is_method_excluded( // // * Private virtual methods are only included in a virtual // implementation. - // - // * Methods accepting pointers are often supplementary - // E.g.: TextServer::font_set_data_ptr() -- in addition to TextServer::font_set_data(). - // These are anyway not accessible in GDScript since that language has no pointers. - // As such support could be added later (if at all), with possibly safe interfaces (e.g. Vec for void*+size pairs) // -- FIXME remove when impl complete #[cfg(not(feature = "codegen-full"))] @@ -768,14 +872,7 @@ fn is_method_excluded( return true; } - method - .return_value - .as_ref() - .map_or(false, |ret| ret.type_.contains('*')) - || method - .arguments - .as_ref() - .map_or(false, |args| args.iter().any(|arg| arg.type_.contains('*'))) + false } #[cfg(feature = "codegen-full")] @@ -996,6 +1093,18 @@ fn make_function_definition( } else { quote! { pub } }; + let (safety, doc) = if function_uses_pointers(method_args, &return_value) { + ( + quote! { unsafe }, + quote! { + #[doc = "# Safety"] + #[doc = ""] + #[doc = "Godot currently does not document safety requirements on this method. Make sure you understand the underlying semantics."] + }, + ) + } else { + (quote! {}, quote! {}) + }; let is_varcall = variant_ffi.is_some(); let fn_name = safe_ident(function_name); @@ -1042,7 +1151,8 @@ fn make_function_definition( if is_virtual { quote! { - fn #fn_name( #receiver #( #params, )* ) #return_decl { + #doc + #safety fn #fn_name( #receiver #( #params, )* ) #return_decl { #call_code } } @@ -1050,7 +1160,8 @@ fn make_function_definition( // varcall (using varargs) let sys_method = &variant_ffi.sys_method; quote! { - #vis fn #fn_name( #receiver #( #params, )* varargs: &[Variant]) #return_decl { + #doc + #vis #safety fn #fn_name( #receiver #( #params, )* varargs: &[Variant]) #return_decl { unsafe { #init_code @@ -1071,7 +1182,8 @@ fn make_function_definition( } else { // ptrcall quote! { - #vis fn #fn_name( #receiver #( #params, )* ) #return_decl { + #doc + #vis #safety fn #fn_name( #receiver #( #params, )* ) #return_decl { unsafe { #init_code diff --git a/godot-codegen/src/context.rs b/godot-codegen/src/context.rs index eee8c872e..53209d111 100644 --- a/godot-codegen/src/context.rs +++ b/godot-codegen/src/context.rs @@ -14,6 +14,7 @@ use std::collections::{HashMap, HashSet}; pub(crate) struct Context<'a> { engine_classes: HashMap, builtin_types: HashSet<&'a str>, + native_structures_types: HashSet<&'a str>, singletons: HashSet<&'a str>, inheritance_tree: InheritanceTree, cached_rust_types: HashMap, @@ -35,6 +36,11 @@ impl<'a> Context<'a> { ctx.builtin_types.insert(ty_name); } + for structure in api.native_structures.iter() { + let ty_name = structure.name.as_str(); + ctx.native_structures_types.insert(ty_name); + } + for class in api.classes.iter() { let class_name = TyName::from_godot(&class.name); @@ -133,6 +139,10 @@ impl<'a> Context<'a> { self.builtin_types.contains(ty_name) } + pub fn is_native_structure(&self, ty_name: &str) -> bool { + self.native_structures_types.contains(ty_name) + } + pub fn is_singleton(&self, class_name: &str) -> bool { self.singletons.contains(class_name) } diff --git a/godot-codegen/src/lib.rs b/godot-codegen/src/lib.rs index 5f76aa6ba..e95159a2b 100644 --- a/godot-codegen/src/lib.rs +++ b/godot-codegen/src/lib.rs @@ -21,7 +21,9 @@ use central_generator::{ generate_core_central_file, generate_core_mod_file, generate_sys_central_file, generate_sys_mod_file, }; -use class_generator::{generate_builtin_class_files, generate_class_files}; +use class_generator::{ + generate_builtin_class_files, generate_class_files, generate_native_structures_files, +}; use context::Context; use interface_generator::generate_sys_interface_file; use util::{ident, to_pascal_case, to_snake_case}; @@ -91,6 +93,15 @@ pub fn generate_core_files(core_gen_path: &Path) { ); watch.record("generate_builtin_class_files"); + generate_native_structures_files( + &api, + &mut ctx, + build_config, + &core_gen_path.join("native"), + &mut out_files, + ); + watch.record("generate_native_structures_files"); + rustfmt_if_needed(out_files); watch.record("rustfmt"); watch.write_stats_to(&core_gen_path.join("codegen-stats.txt")); @@ -131,6 +142,9 @@ enum RustTy { /// `TypedArray` BuiltinArray(TokenStream), + /// C-style raw pointer to a `RustTy`. + RawPointer { inner: Box, is_const: bool }, + /// `TypedArray>` EngineArray { tokens: TokenStream, @@ -168,6 +182,14 @@ impl ToTokens for RustTy { match self { RustTy::BuiltinIdent(ident) => ident.to_tokens(tokens), RustTy::BuiltinArray(path) => path.to_tokens(tokens), + RustTy::RawPointer { + inner, + is_const: true, + } => quote! { *const #inner }.to_tokens(tokens), + RustTy::RawPointer { + inner, + is_const: false, + } => quote! { *mut #inner }.to_tokens(tokens), RustTy::EngineArray { tokens: path, .. } => path.to_tokens(tokens), RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens), RustTy::EngineClass { tokens: path, .. } => path.to_tokens(tokens), @@ -311,6 +333,8 @@ const SELECTED_CLASSES: &[&str] = &[ "SceneTree", "Sprite2D", "SpriteFrames", + "TextServer", + "TextServerExtension", "Texture", "Texture2DArray", "TextureLayered", diff --git a/godot-codegen/src/tests.rs b/godot-codegen/src/tests.rs index 11d3f974d..5851eaa54 100644 --- a/godot-codegen/src/tests.rs +++ b/godot-codegen/src/tests.rs @@ -4,7 +4,9 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::util::{to_pascal_case, to_snake_case}; +use crate::util::{ + parse_native_structures_format, to_pascal_case, to_snake_case, NativeStructuresField, +}; #[test] fn test_pascal_conversion() { @@ -160,3 +162,44 @@ fn test_enumerator_names() { */ ]; } + +#[test] +fn test_parse_native_structures_format() { + // Convenience constructor. + fn native(ty: &str, name: &str) -> NativeStructuresField { + NativeStructuresField { + field_type: String::from(ty), + field_name: String::from(name), + } + } + + assert_eq!(parse_native_structures_format("").unwrap(), vec![]); + + // Check that we handle pointers correctly. + assert_eq!( + parse_native_structures_format("Object *a").unwrap(), + vec![native("Object*", "a"),], + ); + + // Check that we deal with default values correctly. (We currently + // strip and ignore them) + assert_eq!( + parse_native_structures_format("int x = 0").unwrap(), + vec![native("int", "x"),], + ); + + assert_eq!( + parse_native_structures_format("Vector3 position;Vector3 normal;Vector3 collider_velocity;Vector3 collider_angular_velocity;real_t depth;int local_shape;ObjectID collider_id;RID collider;int collider_shape").unwrap(), + vec![ + native("Vector3", "position"), + native("Vector3", "normal"), + native("Vector3", "collider_velocity"), + native("Vector3", "collider_angular_velocity"), + native("real_t", "depth"), + native("int", "local_shape"), + native("ObjectID", "collider_id"), + native("RID", "collider"), + native("int", "collider_shape"), + ], + ); +} diff --git a/godot-codegen/src/util.rs b/godot-codegen/src/util.rs index 66841ef21..02b05b535 100644 --- a/godot-codegen/src/util.rs +++ b/godot-codegen/src/util.rs @@ -4,12 +4,18 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::api_parser::{ClassConstant, Enum}; +use crate::api_parser::{ClassConstant, Enum, MethodArg, MethodReturn}; use crate::special_cases::is_builtin_scalar; use crate::{Context, ModName, RustTy, TyName}; use proc_macro2::{Ident, Literal, TokenStream}; use quote::{format_ident, quote}; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NativeStructuresField { + pub field_type: String, + pub field_name: String, +} + pub fn make_enum_definition(enum_: &Enum) -> TokenStream { // TODO enums which have unique ords could be represented as Rust enums // This would allow exhaustive matches (or at least auto-completed matches + #[non_exhaustive]). But even without #[non_exhaustive], @@ -237,11 +243,34 @@ fn to_hardcoded_rust_type(ty: &str) -> Option<&str> { "enum::Variant.Type" => "VariantType", "enum::Variant.Operator" => "VariantOperator", "enum::Vector3.Axis" => "Vector3Axis", + // Types needed for native structures mapping + "uint8_t" => "u8", + "uint16_t" => "u16", + "uint32_t" => "u32", + "uint64_t" => "u64", + "int8_t" => "i8", + "int16_t" => "i16", + "int32_t" => "i32", + "int64_t" => "i64", + "real_t" => "real", + "void" => "c_void", _ => return None, }; Some(result) } +/// Maps an input type to a Godot type with the same C representation. This is subtly different than [`to_rust_type`], +/// which maps to an appropriate corresponding Rust type. This function should be used in situations where the C ABI for +/// a type must match the Godot equivalent exactly, such as when dealing with pointers. +pub(crate) fn to_rust_type_abi(ty: &str, ctx: &mut Context<'_>) -> RustTy { + match ty { + "int" => RustTy::BuiltinIdent(ident("i32")), + "float" => RustTy::BuiltinIdent(ident("f32")), + "double" => RustTy::BuiltinIdent(ident("f64")), + _ => to_rust_type(ty, ctx), + } +} + /// Maps an _input_ type from the Godot JSON to the corresponding Rust type (wrapping some sort of a token stream). /// /// Uses an internal cache (via `ctx`), as several types are ubiquitous. @@ -268,6 +297,22 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { } } + if ty.ends_with('*') { + // Pointer type; strip '*', see if const, and then resolve the + // inner type. + let mut ty = ty[0..ty.len() - 1].to_string(); + // 'const' should apply to the innermost pointer, if present. + let is_const = ty.starts_with("const ") && !ty.ends_with('*'); + if is_const { + ty = ty.replace("const ", ""); + } + let inner_type = to_rust_type(ty.trim(), ctx); + return RustTy::RawPointer { + inner: Box::new(inner_type), + is_const, + }; + } + if let Some(hardcoded) = to_hardcoded_rust_type(ty) { return RustTy::BuiltinIdent(ident(hardcoded)); } @@ -317,7 +362,7 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { } // Note: do not check if it's a known engine class, because that will not work in minimal mode (since not all classes are stored) - if ctx.is_builtin(ty) { + if ctx.is_builtin(ty) || ctx.is_native_structure(ty) { // Unchanged RustTy::BuiltinIdent(rustify_ty(ty)) } else { @@ -328,3 +373,52 @@ fn to_rust_type_uncached(ty: &str, ctx: &mut Context) -> RustTy { } } } + +/// Parse a string of semicolon-separated C-style type declarations. Fail with `None` if any errors occur. +pub fn parse_native_structures_format(input: &str) -> Option> { + input + .split(';') + .filter(|var| !var.trim().is_empty()) + .map(|var| { + let mut parts = var.trim().splitn(2, ' '); + let mut field_type = parts.next()?.to_owned(); + let mut field_name = parts.next()?.to_owned(); + + // If the field is a pointer, put the star on the type, not + // the name. + if field_name.starts_with('*') { + field_name.remove(0); + field_type.push('*'); + } + + // If Godot provided a default value, ignore it. (TODO We + // might use these if we synthetically generate constructors + // in the future) + if let Some(index) = field_name.find(" = ") { + field_name.truncate(index); + } + + Some(NativeStructuresField { + field_type, + field_name, + }) + }) + .collect() +} + +pub fn function_uses_pointers( + method_args: &Option>, + return_value: &Option<&MethodReturn>, +) -> bool { + if let Some(method_args) = method_args { + if method_args.iter().any(|x| x.type_.contains('*')) { + return true; + } + } + if let Some(return_value) = return_value { + if return_value.type_.contains('*') { + return true; + } + } + false +} diff --git a/godot-core/src/builtin/variant/impls.rs b/godot-core/src/builtin/variant/impls.rs index 4118d2450..c746868c3 100644 --- a/godot-core/src/builtin/variant/impls.rs +++ b/godot-core/src/builtin/variant/impls.rs @@ -263,3 +263,71 @@ impl VariantMetadata for T { sys::GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT32 } } + +impl ToVariant for *mut T { + fn to_variant(&self) -> Variant { + (*self as i64).to_variant() + } +} + +impl ToVariant for *const T { + fn to_variant(&self) -> Variant { + (*self as i64).to_variant() + } +} + +impl FromVariant for *mut T { + fn try_from_variant(variant: &Variant) -> Result { + let n = i64::try_from_variant(variant)?; + Ok(n as Self) + } +} + +impl FromVariant for *const T { + fn try_from_variant(variant: &Variant) -> Result { + let n = i64::try_from_variant(variant)?; + Ok(n as Self) + } +} + +impl VariantMetadata for *mut T { + fn variant_type() -> VariantType { + VariantType::Int + } + + fn property_info(property_name: &str) -> PropertyInfo { + PropertyInfo { + variant_type: Self::variant_type(), + class_name: Self::class_name(), + property_name: StringName::from(property_name), + hint: global::PropertyHint::PROPERTY_HINT_INT_IS_POINTER, + hint_string: GodotString::from("pointer"), + usage: global::PropertyUsageFlags::PROPERTY_USAGE_DEFAULT, + } + } + + fn param_metadata() -> sys::GDExtensionClassMethodArgumentMetadata { + sys::GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64 + } +} + +impl VariantMetadata for *const T { + fn variant_type() -> VariantType { + VariantType::Int + } + + fn property_info(property_name: &str) -> PropertyInfo { + PropertyInfo { + variant_type: Self::variant_type(), + class_name: Self::class_name(), + property_name: StringName::from(property_name), + hint: global::PropertyHint::PROPERTY_HINT_INT_IS_POINTER, + hint_string: GodotString::from("pointer"), + usage: global::PropertyUsageFlags::PROPERTY_USAGE_DEFAULT, + } + } + + fn param_metadata() -> sys::GDExtensionClassMethodArgumentMetadata { + sys::GDEXTENSION_METHOD_ARGUMENT_METADATA_INT_IS_INT64 + } +} diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index bced3b43a..47e79d174 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -13,6 +13,7 @@ pub mod export; pub mod init; pub mod log; pub mod macros; +pub mod native_structure; pub mod obj; pub use godot_ffi as sys; diff --git a/godot-core/src/macros.rs b/godot-core/src/macros.rs index eaec63000..68d842bb5 100644 --- a/godot-core/src/macros.rs +++ b/godot-core/src/macros.rs @@ -416,7 +416,7 @@ macro_rules! gdext_virtual_method_callback_inner { $ptrcall:ident, $Class:ty, $map_method:ident, - fn $method_name:ident( + $( unsafe )? fn $method_name:ident( $( $arg:ident : $Param:ty, )* ) -> $Ret:ty ) => { @@ -486,6 +486,44 @@ macro_rules! gdext_virtual_method_callback { ) }; + // immutable + ( + $Class:ty, + unsafe fn $method_name:ident( + &self + $(, $param:ident : $ParamTy:ty)* + $(,)? + ) -> $RetTy:ty + ) => { + $crate::gdext_virtual_method_callback_inner!( + ref, + $Class, + map, + unsafe fn $method_name( + $( $param : $ParamTy, )* + ) -> $RetTy + ) + }; + + // mutable + ( + $Class:ty, + unsafe fn $method_name:ident( + &mut self + $(, $param:ident : $ParamTy:ty)* + $(,)? + ) -> $RetTy:ty + ) => { + $crate::gdext_virtual_method_callback_inner!( + mut, + $Class, + map_mut, + unsafe fn $method_name( + $( $param : $ParamTy, )* + ) -> $RetTy + ) + }; + // immutable without return type ( $Class:ty, @@ -523,6 +561,43 @@ macro_rules! gdext_virtual_method_callback { ) -> () ) }; + // immutable without return type (UNSAFE) + ( + $Class:ty, + unsafe fn $method_name:ident( + &self + $(, $param:ident : $ParamTy:ty)* + $(,)? + ) + ) => { + // recurse this macro + $crate::gdext_virtual_method_callback!( + $Class, + unsafe fn $method_name( + &self + $(, $param : $ParamTy )* + ) -> () + ) + }; + + // mutable without return type (UNSAFE) + ( + $Class:ty, + unsafe fn $method_name:ident( + &mut self + $(, $param:ident : $ParamTy:ty)* + $(,)? + ) + ) => { + // recurse this macro + $crate::gdext_virtual_method_callback!( + $Class, + unsafe fn $method_name( + &mut self + $(, $param : $ParamTy )* + ) -> () + ) + }; } #[macro_export] diff --git a/godot-core/src/native_structure.rs b/godot-core/src/native_structure.rs new file mode 100644 index 000000000..65a4246d8 --- /dev/null +++ b/godot-core/src/native_structure.rs @@ -0,0 +1,7 @@ +/* + * 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/. + */ + +pub use crate::gen::native::*; diff --git a/godot-ffi/src/godot_ffi.rs b/godot-ffi/src/godot_ffi.rs index 0230c66b3..0afec356b 100644 --- a/godot-ffi/src/godot_ffi.rs +++ b/godot-ffi/src/godot_ffi.rs @@ -434,6 +434,14 @@ mod scalars { impl_godot_marshalling!(f32 as f64; lossy); + unsafe impl GodotFfi for *const T { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } + } + + unsafe impl GodotFfi for *mut T { + ffi_methods! { type sys::GDExtensionTypePtr = *mut Self; .. } + } + unsafe impl GodotFfi for () { unsafe fn from_sys(_ptr: sys::GDExtensionTypePtr) -> Self { // Do nothing diff --git a/godot/src/lib.rs b/godot/src/lib.rs index 3488351b5..1d781eb57 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -138,7 +138,7 @@ //! at any time. #[doc(inline)] -pub use godot_core::{builtin, engine, export, log, obj, sys}; +pub use godot_core::{builtin, engine, export, log, native_structure, obj, sys}; /// Facilities for initializing and terminating the GDExtension library. pub mod init { diff --git a/itest/rust/src/lib.rs b/itest/rust/src/lib.rs index 652528406..276521da6 100644 --- a/itest/rust/src/lib.rs +++ b/itest/rust/src/lib.rs @@ -21,6 +21,7 @@ mod enum_test; mod export_test; mod gdscript_ffi_test; mod init_test; +mod native_structures_test; mod node_test; mod object_test; mod packed_array_test; diff --git a/itest/rust/src/native_structures_test.rs b/itest/rust/src/native_structures_test.rs new file mode 100644 index 000000000..c3b0f180e --- /dev/null +++ b/itest/rust/src/native_structures_test.rs @@ -0,0 +1,141 @@ +/* + * 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/. + */ + +use crate::itest; +use godot::engine::text_server::Direction; +use godot::engine::{TextServer, TextServerExtension, TextServerExtensionVirtual}; +use godot::native_structure::{AudioFrame, CaretInfo, Glyph}; +use godot::prelude::{godot_api, Base, Gd, GodotClass, Rect2, Rid, Share, Variant}; + +use std::cell::Cell; + +#[derive(GodotClass)] +#[class(base=TextServerExtension)] +pub struct TestTextServer { + #[base] + base: Base, + glyphs: [Glyph; 2], + cell: Cell>, +} + +// Simple function to make these up with one field differing. +fn sample_glyph(start: i32) -> Glyph { + Glyph { + start, + end: 8, + count: 9, + repeat: 10, + flags: 33, + x_off: 999.0, + y_off: -999.0, + advance: 132.0, + font_rid: Rid::new(1024), + font_size: 1025, + index: 1026, + } +} + +#[godot_api] +impl TextServerExtensionVirtual for TestTextServer { + fn init(base: Base) -> Self { + TestTextServer { + base, + glyphs: [sample_glyph(99), sample_glyph(700)], + cell: Cell::new(None), + } + } + + unsafe fn shaped_text_get_carets(&self, shaped: Rid, position: i64, caret: *mut CaretInfo) { + // Record the arguments we were called with. + self.cell.set(Some((shaped, position))); + // Now put something in the out param. + *caret = CaretInfo { + leading_caret: Rect2::from_components(0.0, 0.0, 0.0, 0.0), + trailing_caret: Rect2::from_components(1.0, 1.0, 1.0, 1.0), + leading_direction: Direction::DIRECTION_AUTO, + trailing_direction: Direction::DIRECTION_LTR, + }; + } + + fn shaped_text_get_glyph_count(&self, _shaped: Rid) -> i64 { + self.glyphs.len() as i64 + } + + unsafe fn shaped_text_get_glyphs(&self, _shaped: Rid) -> *const Glyph { + self.glyphs.as_ptr() + } +} + +#[itest] +fn test_native_structures() { + // Test construction of a few simple types. + let _ = AudioFrame { + left: 0.0, + right: 0.0, + }; + let _ = Glyph { + start: 0, + end: 0, + count: 0, + repeat: 0, + flags: 0, + x_off: 0.0, + y_off: 0.0, + advance: 0.0, + font_rid: Rid::new(0), + font_size: 0, + index: 0, + }; +} + +#[itest] +fn test_native_structure_out_parameter() { + // Instantiate a TextServerExtension and then have Godot call a + // function which uses an 'out' pointer parameter. + let mut ext: Gd = Gd::new_default(); + let result = ext + .share() + .upcast::() + .shaped_text_get_carets(Rid::new(100), 200); + + // Check that we called the virtual function. + let cell = ext.bind_mut().cell.take(); + assert_eq!(cell, Some((Rid::new(100), 200))); + + // Check the result dictionary (Godot made it out of our 'out' + // param). + assert_eq!( + result.get("leading_rect"), + Some(Variant::from(Rect2::from_components(0.0, 0.0, 0.0, 0.0))) + ); + assert_eq!( + result.get("trailing_rect"), + Some(Variant::from(Rect2::from_components(1.0, 1.0, 1.0, 1.0))) + ); + assert_eq!( + result.get("leading_direction"), + Some(Variant::from(Direction::DIRECTION_AUTO)) + ); + assert_eq!( + result.get("trailing_direction"), + Some(Variant::from(Direction::DIRECTION_LTR)) + ); +} + +#[itest] +fn test_native_structure_pointer_to_array_parameter() { + // Instantiate a TextServerExtension. + let ext: Gd = Gd::new_default(); + let result = ext + .share() + .upcast::() + .shaped_text_get_glyphs(Rid::new(100)); + + // Check the result array. + assert_eq!(result.len(), 2); + assert_eq!(result.get(0).get("start"), Some(Variant::from(99))); + assert_eq!(result.get(1).get("start"), Some(Variant::from(700))); +}