use crate::api::*;
use crate::class_docs::GodotXmlDocs;
use crate::rust_safe_name;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use std::collections::HashMap;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) enum IcallType {
#[cfg(feature = "ptrcall")]
Ptr,
Varargs,
Var,
}
#[cfg_attr(not(feature = "ptrcall"), allow(dead_code))]
pub(crate) struct MethodSig {
pub(crate) return_type: Ty,
pub(crate) arguments: Vec<Ty>,
pub(crate) has_varargs: bool,
}
impl MethodSig {
pub(crate) fn from_method(method: &GodotMethod) -> Self {
fn ty_erase(ty: Ty) -> Ty {
match ty {
Ty::Vector3Axis
| Ty::Result
| Ty::VariantType
| Ty::VariantOperator
| Ty::Enum(_) => Ty::I64,
Ty::Object(path) => Ty::Object(path),
other => other,
}
}
let has_varargs = method.has_varargs;
let return_type = if has_varargs {
Ty::Variant
} else {
Ty::from_src(&method.return_type)
};
let mut args = Vec::new();
for arg in &method.arguments {
args.push(ty_erase(arg.get_type()));
}
Self {
return_type: ty_erase(return_type),
arguments: args,
has_varargs: method.has_varargs,
}
}
pub(crate) fn function_name(&self) -> String {
fn ty_arg_name(ty: &Ty) -> &'static str {
match ty {
Ty::Void => "void",
Ty::String => "str",
Ty::F64 => "f64",
Ty::I64 => "i64",
Ty::Bool => "bool",
Ty::Vector2 => "vec2",
Ty::Vector3 => "vec3",
Ty::Quat => "quat",
Ty::Transform => "trans",
Ty::Transform2D => "trans2D",
Ty::Rect2 => "rect2",
Ty::Plane => "plane",
Ty::Basis => "basis",
Ty::Color => "color",
Ty::NodePath => "nodepath",
Ty::Variant => "var",
Ty::Aabb => "aabb",
Ty::Rid => "rid",
Ty::VariantArray => "arr",
Ty::Dictionary => "dict",
Ty::ByteArray => "bytearr",
Ty::StringArray => "strarr",
Ty::Vector2Array => "vec2arr",
Ty::Vector3Array => "vec3arr",
Ty::ColorArray => "colorarr",
Ty::Int32Array => "i32arr",
Ty::Float32Array => "f32arr",
Ty::Result
| Ty::Vector3Axis
| Ty::VariantType
| Ty::VariantOperator
| Ty::Enum(_) => "i64",
Ty::Object(_) => "obj",
}
}
let icall_ty = self.icall_type();
let mut name = match icall_ty {
#[cfg(feature = "ptrcall")]
IcallType::Ptr => format!("icallptr_{}", ty_arg_name(&self.return_type)),
IcallType::Varargs => String::from("icallvarargs_"),
IcallType::Var => String::from("icallvar_"),
};
for arg in &self.arguments {
name.push('_');
name.push_str(ty_arg_name(arg));
}
name
}
#[allow(clippy::single_match)]
#[cfg(feature = "ptrcall")]
pub(crate) fn icall_type(&self) -> IcallType {
if self.has_varargs {
return IcallType::Varargs;
}
match self.return_type {
Ty::VariantArray => return IcallType::Var,
_ => {}
}
IcallType::Ptr
}
#[cfg(not(feature = "ptrcall"))]
pub(crate) fn icall_type(&self) -> IcallType {
if self.has_varargs {
return IcallType::Varargs;
}
IcallType::Var
}
}
fn skip_method(method: &GodotMethod, name: &str) -> bool {
const METHODS: &[&str] = &["free", "reference", "unreference", "init_ref"];
METHODS.contains(&name) || method.is_virtual
}
pub fn generate_method_table(api: &Api, class: &GodotClass) -> TokenStream {
let has_underscore = api.api_underscore.contains(&class.name);
let method_table = format_ident!("{}MethodTable", class.name);
let lookup_name: String = if has_underscore {
format!("_{class}\0", class = class.name)
} else {
format!("{}\0", class.name)
};
let struct_methods = class.methods.iter().filter_map(|m| {
let rust_name = m.get_name().rust_name;
let rust_ident = format_ident!("{}", rust_name);
if !skip_method(m, rust_name) {
Some(quote! { pub #rust_ident: *mut sys::godot_method_bind })
} else {
None
}
});
let struct_definition = quote! {
#[doc(hidden)]
#[allow(non_camel_case_types, dead_code)]
pub(crate) struct #method_table {
pub class_constructor: sys::godot_class_constructor,
#(#struct_methods),*
}
};
let impl_methods = class.methods.iter().filter_map(|m| {
let rust_name = m.get_name().rust_name;
let rust_ident = format_ident!("{}", rust_name);
if !skip_method(m, rust_name) {
Some(quote! { #rust_ident: 0 as *mut sys::godot_method_bind })
} else {
None
}
});
let init_methods = class.methods.iter().filter_map(|m| {
let MethodName {
rust_name,
original_name,
} = m.get_name();
let rust_ident = format_ident!("{}", rust_name);
let original_name = format!("{original_name}\0");
if !skip_method(m, rust_name) {
assert!(original_name.ends_with('\0'), "original_name must be null terminated");
Some(quote! {
table.#rust_ident = (gd_api.godot_method_bind_get_method)(class_name, #original_name.as_ptr() as *const c_char );
})
} else {
None
}
});
assert!(
lookup_name.ends_with('\0'),
"lookup_name must be null terminated"
);
let methods = quote! {
impl #method_table {
unsafe fn get_mut() -> &'static mut Self {
static mut TABLE: #method_table = #method_table {
class_constructor: None,
#(#impl_methods),*
};
&mut TABLE
}
#[inline]
#[allow(dead_code)]
pub fn get(gd_api: &GodotApi) -> &'static Self {
unsafe {
let table = Self::get_mut();
static INIT: std::sync::Once = std::sync::Once::new();
INIT.call_once(|| {
#method_table::init(table, gd_api);
});
table
}
}
#[inline(never)]
#[allow(dead_code)]
fn init(table: &mut Self, gd_api: &GodotApi) {
unsafe {
let class_name = #lookup_name.as_ptr() as *const c_char;
table.class_constructor = (gd_api.godot_get_class_constructor)(class_name);
#(#init_methods)*
}
}
}
};
quote! {
#struct_definition
#methods
}
}
fn rename_property_getter<'a>(name: &'a str, class: &GodotClass) -> &'a str {
if name.starts_with("get_") && class.is_getter(name) {
&name[4..]
} else {
name
}
}
const UNSAFE_OBJECT_METHODS: &[(&str, &str)] = &[
("Object", "call"),
("Object", "callv"),
("Object", "call_deferred"),
];
pub(crate) fn generate_methods(
class: &GodotClass,
icalls: &mut HashMap<String, MethodSig>,
docs: Option<&GodotXmlDocs>,
) -> TokenStream {
struct Generated {
icall: proc_macro2::Ident,
icall_ty: IcallType,
maybe_unsafe: TokenStream,
maybe_unsafe_reason: &'static str,
}
fn arg_erase(ty: &Ty, name: &proc_macro2::Ident) -> TokenStream {
match ty {
Ty::VariantType | Ty::VariantOperator | Ty::Vector3Axis | Ty::Result => {
quote! { (#name as u32) as i64 }
}
Ty::Variant => quote! { #name.owned_to_variant() },
Ty::String | Ty::NodePath => quote! { #name.into() },
Ty::Enum(_) => quote! { #name.0 },
Ty::Object(_) => quote! { #name.as_arg_ptr() },
Ty::I64 | Ty::F64 | Ty::Bool => quote! { #name as _ },
_ => quote! { #name },
}
}
let mut generated = HashMap::new();
let mut result = TokenStream::new();
for method in &class.methods {
let MethodName {
rust_name: method_name,
..
} = method.get_name();
if skip_method(method, method_name) {
continue;
}
let mut ret_type = method.get_return_type();
let mut rust_ret_type = ret_type.to_rust();
if generated.contains_key(method_name) {
continue;
}
let mut params_decl = TokenStream::new();
let mut params_use = TokenStream::new();
for argument in &method.arguments {
let ty = argument.get_type();
let rust_ty = ty.to_rust_arg();
let name = rust_safe_name(&argument.name);
let arg_erased = arg_erase(&ty, &name);
params_decl.extend(quote! {
, #name: #rust_ty
});
params_use.extend(quote! {
, #arg_erased
});
}
if method.has_varargs {
params_decl.extend(quote! {
, varargs: &[Variant]
});
params_use.extend(quote! {
, varargs
});
ret_type = Ty::Variant;
rust_ret_type = syn::parse_quote! { Variant };
}
let rusty_method_name = rename_property_getter(method_name, class);
let method_sig = MethodSig::from_method(method);
let icall_ty = method_sig.icall_type();
let icall_name = method_sig.function_name();
let icall = format_ident!("{}", icall_name);
let maybe_unsafe: TokenStream;
let maybe_unsafe_reason: &str;
if let Some(unsafe_reason) = unsafe_reason(class, method_name, &method_sig) {
maybe_unsafe = quote! { unsafe };
maybe_unsafe_reason = unsafe_reason;
} else {
maybe_unsafe = TokenStream::default();
maybe_unsafe_reason = "";
}
icalls.insert(icall_name.clone(), method_sig);
let rusty_name = rust_safe_name(rusty_method_name);
let method_bind_fetch = {
let method_table = format_ident!("{}MethodTable", class.name);
let rust_method_name = format_ident!("{}", method_name);
quote! {
let method_bind: *mut sys::godot_method_bind = #method_table::get(get_api()).#rust_method_name;
}
};
let doc_comment = docs
.and_then(|docs| docs.get_class_method_desc(class.name.as_str(), method_name))
.unwrap_or("");
let recover = ret_recover(&ret_type, icall_ty);
let output = quote! {
#[doc = #doc_comment]
#[doc = #maybe_unsafe_reason]
#[inline]
pub #maybe_unsafe fn #rusty_name(&self #params_decl) -> #rust_ret_type {
unsafe {
#method_bind_fetch
let ret = crate::icalls::#icall(method_bind, self.this.sys().as_ptr() #params_use);
#recover
}
}
};
result.extend(output);
generated.insert(
method_name.to_string(),
Generated {
icall,
icall_ty,
maybe_unsafe,
maybe_unsafe_reason,
},
);
}
for property in &class.properties {
if property.index < 0 || property.name.contains('/') {
continue;
}
let property_index = property.index;
let ty = Ty::from_src(&property.type_);
if let Some(Generated {
icall,
icall_ty,
maybe_unsafe,
maybe_unsafe_reason,
}) = generated.get(&property.getter)
{
let rusty_name = rust_safe_name(&property.name);
let rust_ret_type = ty.to_rust();
let method_bind_fetch = {
let method_table = format_ident!("{}MethodTable", class.name);
let rust_method_name = format_ident!("{}", property.getter);
quote! {
let method_bind: *mut sys::godot_method_bind = #method_table::get(get_api()).#rust_method_name;
}
};
let doc_comment = docs
.and_then(|docs| {
docs.get_class_method_desc(
class.name.as_str(),
&format!("get_{}", property.name),
)
})
.unwrap_or("");
let recover = ret_recover(&ty, *icall_ty);
let output = quote! {
#[doc = #doc_comment]
#[doc = #maybe_unsafe_reason]
#[inline]
pub #maybe_unsafe fn #rusty_name(&self) -> #rust_ret_type {
unsafe {
#method_bind_fetch
let ret = crate::icalls::#icall(method_bind, self.this.sys().as_ptr(), #property_index);
#recover
}
}
};
result.extend(output);
}
if let Some(Generated {
icall,
icall_ty,
maybe_unsafe,
maybe_unsafe_reason,
}) = generated.get(&property.setter)
{
let rusty_name = rust_safe_name(&format!("set_{}", property.name));
let rust_arg_ty = ty.to_rust_arg();
let arg_ident = format_ident!("value");
let arg_erased = arg_erase(&ty, &arg_ident);
let method_bind_fetch = {
let method_table = format_ident!("{}MethodTable", class.name);
let rust_method_name = format_ident!("{}", property.setter);
quote! {
let method_bind: *mut sys::godot_method_bind = #method_table::get(get_api()).#rust_method_name;
}
};
let doc_comment = docs
.and_then(|docs| {
docs.get_class_method_desc(
class.name.as_str(),
&format!("set_{}", property.name),
)
})
.unwrap_or("");
let recover = ret_recover(&Ty::Void, *icall_ty);
let output = quote! {
#[doc = #doc_comment]
#[doc = #maybe_unsafe_reason]
#[inline]
pub #maybe_unsafe fn #rusty_name(&self, #arg_ident: #rust_arg_ty) {
unsafe {
#method_bind_fetch
let ret = crate::icalls::#icall(method_bind, self.this.sys().as_ptr(), #property_index, #arg_erased);
#recover
}
}
};
result.extend(output);
}
}
result
}
fn unsafe_reason(
class: &GodotClass,
method_name: &str,
method_sig: &MethodSig,
) -> Option<&'static str> {
if UNSAFE_OBJECT_METHODS.contains(&(&class.name, method_name)) {
Some(
"\n# Safety\
\nThis function bypasses Rust's static type checks (aliasing, thread boundaries, calls to free(), ...).",
)
} else if method_sig.arguments.contains(&Ty::Rid) {
Some(
"\n# Safety\
\nThis function has parameters of type `Rid` (resource ID). \
RIDs are untyped and interpreted as raw pointers by the engine, so passing an incorrect RID can cause UB.")
} else {
None
}
}
fn ret_recover(ty: &Ty, icall_ty: IcallType) -> TokenStream {
match icall_ty {
#[cfg(feature = "ptrcall")]
IcallType::Ptr => ty.to_return_post(),
IcallType::Varargs => {
quote! { ret }
}
IcallType::Var => ty.to_return_post_variant(),
}
}
pub(crate) fn generate_icall(name: String, sig: MethodSig) -> TokenStream {
match sig.icall_type() {
#[cfg(feature = "ptrcall")]
IcallType::Ptr => ptrcall::generate_icall(name, sig),
IcallType::Varargs => varargs_call::generate_icall(name, sig),
IcallType::Var => varcall::generate_icall(name, sig),
}
}
#[cfg(feature = "ptrcall")]
mod ptrcall {
use super::*;
use quote::{format_ident, quote};
pub(super) fn generate_icall(name: String, sig: super::MethodSig) -> proc_macro2::TokenStream {
let name_ident = format_ident!("{}", name);
let rust_ret_type = sig.return_type.to_icall_return();
let arguments = sig
.arguments
.iter()
.enumerate()
.map(|(i, ty)| (format_ident!("arg{}", i), ty));
let args = arguments.clone().map(|(name, ty)| {
let typ = ty.to_icall_arg();
quote! {
#name: #typ
}
});
let arg_count = sig.arguments.len();
let method_body = {
let args = arguments
.clone()
.map(|(name, ty)| generate_argument_pre(ty, name));
let return_pre = generate_return_pre(&sig.return_type);
let arg_forgets = arguments.clone().map(|(name, _)| {
quote! { let #name = ::std::mem::ManuallyDrop::new(#name); }
});
let arg_drops = arguments.clone().map(|(name, _)| {
quote! { ::std::mem::ManuallyDrop::into_inner(#name); }
});
quote! {
let gd_api = get_api();
let mut argument_buffer : [*const libc::c_void; #arg_count] = [
#(#args),*
];
#(#arg_forgets)*
#return_pre
(gd_api.godot_method_bind_ptrcall)(method_bind, obj_ptr, argument_buffer.as_mut_ptr() as *mut _, ret_ptr as *mut _);
#(#arg_drops)*
ret
}
};
quote! {
#[doc(hidden)]
#[inline(never)]
pub(crate) unsafe fn #name_ident(method_bind: *mut sys::godot_method_bind, obj_ptr: *mut sys::godot_object, #(#args,)*) -> #rust_ret_type {
#method_body
}
}
}
fn generate_argument_pre(ty: &Ty, name: proc_macro2::Ident) -> TokenStream {
match ty {
Ty::Bool
| Ty::F64
| Ty::I64
| Ty::Vector2
| Ty::Vector3
| Ty::Transform
| Ty::Transform2D
| Ty::Quat
| Ty::Plane
| Ty::Aabb
| Ty::Basis
| Ty::Rect2
| Ty::Color => {
quote! {
(&#name) as *const _ as *const _
}
}
Ty::Variant
| Ty::String
| Ty::Rid
| Ty::NodePath
| Ty::VariantArray
| Ty::Dictionary
| Ty::ByteArray
| Ty::StringArray
| Ty::Vector2Array
| Ty::Vector3Array
| Ty::ColorArray
| Ty::Int32Array
| Ty::Float32Array => {
quote! {
#name.sys() as *const _ as *const _
}
}
Ty::Object(_) => {
quote! {
#name as *const _ as *const _
}
}
_ => Default::default(),
}
}
fn generate_return_pre(ty: &Ty) -> TokenStream {
match ty {
Ty::Void => {
quote! {
let ret = ();
let ret_ptr = ptr::null_mut();
}
}
Ty::F64 => {
quote! {
let mut ret = 0.0f64;
let ret_ptr = &mut ret as *mut _;
}
}
Ty::I64 => {
quote! {
let mut ret = 0i64;
let ret_ptr = &mut ret as *mut _;
}
}
Ty::Bool => {
quote! {
let mut ret = false;
let ret_ptr = &mut ret as *mut _;
}
}
Ty::String
| Ty::Vector2
| Ty::Vector3
| Ty::Transform
| Ty::Transform2D
| Ty::Quat
| Ty::Plane
| Ty::Rect2
| Ty::Basis
| Ty::Color
| Ty::NodePath
| Ty::Variant
| Ty::Aabb
| Ty::VariantArray
| Ty::Dictionary
| Ty::ByteArray
| Ty::StringArray
| Ty::Vector2Array
| Ty::Vector3Array
| Ty::ColorArray
| Ty::Int32Array
| Ty::Float32Array
| Ty::Rid => {
let sys_ty = ty.to_sys().unwrap();
quote! {
let mut ret = #sys_ty::default();
let ret_ptr = (&mut ret) as *mut _;
}
}
Ty::Object(_) => {
quote! {
let mut ret: *mut sys::godot_object = ptr::null_mut();
let ret_ptr = (&mut ret) as *mut _;
}
}
Ty::Result | Ty::VariantType | Ty::VariantOperator | Ty::Vector3Axis | Ty::Enum(_) => {
quote! {
let mut ret = 0i64;
let ret_ptr = (&mut ret) as *mut _;
}
}
}
}
}
mod varargs_call {
use crate::api::*;
use quote::{format_ident, quote};
pub(super) fn generate_icall(name: String, sig: super::MethodSig) -> proc_macro2::TokenStream {
let name_ident = format_ident!("{}", name);
let arguments = sig
.arguments
.iter()
.enumerate()
.map(|(i, ty)| (format_ident!("arg{}", i), ty));
let args = arguments.clone().map(|(name, ty)| {
let rust_ty = ty.to_icall_arg();
quote! {
#name: #rust_ty
}
});
let arg_count = sig.arguments.len();
let method_body = {
let args_buffer = quote! {
let mut argument_buffer: std::vec::Vec<*const sys::godot_variant> = std::vec::Vec::with_capacity(#arg_count + varargs.len());
};
let args = arguments.clone().map(|(name, ty)| {
let new_var = match ty {
Ty::Object(_) => {
quote! {
let #name: Variant = Variant::from_object_ptr(#name);
}
}
_ => {
quote! {
let #name: Variant = (&#name).to_variant();
}
}
};
quote! {
#new_var
argument_buffer.push(#name.sys());
}
});
let varargs_body = quote! {
for arg in varargs {
argument_buffer.push(arg.sys() as *const _);
}
let ret = (gd_api.godot_method_bind_call)(method_bind, obj_ptr, argument_buffer.as_mut_ptr(), argument_buffer.len() as _, ptr::null_mut());
};
let drop_args = arguments.clone().map(|(name, _)| {
quote! { drop(#name); }
});
quote! {
let gd_api = get_api();
#args_buffer
#(#args)*
#varargs_body
#(#drop_args)*
Variant::from_sys(ret)
}
};
quote! {
#[doc(hidden)]
#[inline(never)]
pub(crate) unsafe fn #name_ident(method_bind: *mut sys::godot_method_bind, obj_ptr: *mut sys::godot_object, #(#args,)* varargs: &[Variant]) -> Variant {
#method_body
}
}
}
}
mod varcall {
use crate::api::*;
use quote::{format_ident, quote};
pub(super) fn generate_icall(name: String, sig: super::MethodSig) -> proc_macro2::TokenStream {
let name_ident = format_ident!("{}", name);
let arguments = sig
.arguments
.iter()
.enumerate()
.map(|(i, ty)| (format_ident!("arg{}", i), ty));
let args = arguments.clone().map(|(name, ty)| {
let rust_ty = ty.to_icall_arg();
quote! {
#name: #rust_ty
}
});
let arg_count = sig.arguments.len();
let method_body = {
let arg_bindings = arguments.clone().map(|(name, ty)| match ty {
Ty::Object(_) => {
quote! {
let #name: Variant = Variant::from_object_ptr(#name);
}
}
_ => {
quote! {
let #name: Variant = (&#name).to_variant();
}
}
});
let args_buffer = {
let arg_assigns = arguments.clone().map(|(name, _)| quote! {#name});
quote! {
let mut argument_buffer: [*const sys::godot_variant; #arg_count] = [
#(#arg_assigns.sys() as *const _),*
];
}
};
let body = quote! {
let ret = (gd_api.godot_method_bind_call)(method_bind, obj_ptr, argument_buffer.as_mut_ptr(), argument_buffer.len() as _, ptr::null_mut());
};
let drop_args = arguments.clone().map(|(name, _)| {
quote! { drop(#name); }
});
quote! {
let gd_api = get_api();
#(#arg_bindings)*
#args_buffer
#body
#(#drop_args)*
Variant::from_sys(ret)
}
};
quote! {
#[doc(hidden)]
#[inline(never)]
pub(crate) unsafe fn #name_ident(method_bind: *mut sys::godot_method_bind, obj_ptr: *mut sys::godot_object, #(#args,)*) -> Variant {
#method_body
}
}
}
}