#![allow(dead_code)]
use std::collections::HashMap;
use std::fmt;
use proc_macro2::{Ident, Literal, TokenStream};
use quote::{ToTokens, format_ident, quote};
use crate::context::Context;
use crate::conv;
use crate::models::json::{JsonBuiltinClass, JsonMethodArg, JsonMethodReturn};
use crate::util::{ident, option_as_slice, safe_ident};
mod enums;
pub use enums::{Enum, Enumerator, EnumeratorValue};
pub struct ExtensionApi {
pub builtins: Vec<BuiltinVariant>,
pub classes: Vec<Class>,
pub singletons: Vec<Singleton>,
pub native_structures: Vec<NativeStructure>,
pub utility_functions: Vec<UtilityFunction>,
pub global_enums: Vec<Enum>,
pub godot_version: GodotApiVersion,
pub builtin_sizes: Vec<BuiltinSize>,
}
impl ExtensionApi {
pub fn builtin_by_original_name(&self, name: &str) -> &BuiltinVariant {
self.builtins
.iter()
.find(|b| b.godot_original_name() == name)
.unwrap_or_else(|| panic!("builtin_by_name: invalid `{name}`"))
}
}
pub struct ApiView<'a> {
class_by_ty: HashMap<TyName, &'a Class>,
}
impl<'a> ApiView<'a> {
pub fn new(api: &'a ExtensionApi) -> ApiView<'a> {
let class_by_ty = api.classes.iter().map(|c| (c.name().clone(), c)).collect();
Self { class_by_ty }
}
pub fn get_engine_class(&self, ty: &TyName) -> &'a Class {
self.class_by_ty
.get(ty)
.unwrap_or_else(|| panic!("specified type `{}` is not an engine class", ty.godot_ty))
}
}
pub trait ClassLike {
fn common(&self) -> &ClassCommons;
fn name(&self) -> &TyName {
&self.common().name
}
fn mod_name(&self) -> &ModName {
&self.common().mod_name
}
}
pub struct ClassCommons {
pub name: TyName,
pub mod_name: ModName,
}
pub struct BuiltinClass {
pub common: ClassCommons,
pub inner_name: TyName,
pub methods: Vec<BuiltinMethod>,
pub constructors: Vec<Constructor>,
pub operators: Vec<Operator>,
pub has_destructor: bool,
pub enums: Vec<Enum>,
}
impl BuiltinClass {
pub fn inner_name(&self) -> &Ident {
&self.inner_name.rust_ty
}
}
impl ClassLike for BuiltinClass {
fn common(&self) -> &ClassCommons {
&self.common
}
}
pub struct BuiltinVariant {
pub godot_original_name: String,
pub godot_shout_name: String,
pub godot_snake_name: String,
pub builtin_class: Option<BuiltinClass>,
pub variant_type_ord: i32,
}
impl BuiltinVariant {
pub fn godot_original_name(&self) -> &str {
&self.godot_original_name
}
pub fn godot_shout_name(&self) -> &str {
&self.godot_shout_name
}
pub fn snake_name(&self) -> &str {
&self.godot_snake_name
}
pub fn associated_builtin_class(&self) -> Option<&BuiltinClass> {
self.builtin_class.as_ref()
}
pub fn sys_variant_type(&self) -> Ident {
format_ident!("GDEXTENSION_VARIANT_TYPE_{}", self.godot_shout_name())
}
pub fn unsuffixed_ord_lit(&self) -> Literal {
Literal::i32_unsuffixed(self.variant_type_ord)
}
}
pub struct Class {
pub common: ClassCommons,
pub is_refcounted: bool,
pub is_instantiable: bool,
pub is_experimental: bool,
pub is_final: bool,
pub base_class: Option<TyName>,
pub api_level: ClassCodegenLevel,
pub constants: Vec<ClassConstant>,
pub enums: Vec<Enum>,
pub methods: Vec<ClassMethod>,
pub signals: Vec<ClassSignal>,
}
impl ClassLike for Class {
fn common(&self) -> &ClassCommons {
&self.common
}
}
pub struct NativeStructure {
pub name: String,
pub format: String,
}
pub struct Singleton {
pub name: TyName,
}
trait Constant {
fn name(&self) -> &str;
}
pub struct ClassConstant {
pub name: String,
pub value: ClassConstantValue,
}
pub enum ClassConstantValue {
I32(i32),
I64(i64),
}
pub struct Operator {
pub symbol: String,
}
pub struct Constructor {
pub index: usize,
pub raw_parameters: Vec<JsonMethodArg>,
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub enum BuildConfiguration {
Float32,
Float64,
Double32,
Double64,
}
impl BuildConfiguration {
#[cfg(feature = "double-precision")] #[cfg_attr(published_docs, doc(cfg(feature = "double-precision")))]
pub fn is_applicable(self) -> bool {
matches!(self, Self::Double32 | Self::Double64)
}
#[cfg(not(feature = "double-precision"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "double-precision"))))]
pub fn is_applicable(self) -> bool {
matches!(self, Self::Float32 | Self::Float64)
}
#[cfg(feature = "double-precision")] #[cfg_attr(published_docs, doc(cfg(feature = "double-precision")))]
pub fn all_applicable() -> [BuildConfiguration; 2] {
[BuildConfiguration::Double32, BuildConfiguration::Double64]
}
#[cfg(not(feature = "double-precision"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "double-precision"))))]
pub fn all_applicable() -> [BuildConfiguration; 2] {
[BuildConfiguration::Float32, BuildConfiguration::Float64]
}
pub fn is_64bit(self) -> bool {
matches!(self, Self::Float64 | Self::Double64)
}
}
pub struct BuiltinSize {
pub builtin_original_name: String,
pub config: BuildConfiguration,
pub size: usize,
}
#[derive(Clone)]
pub struct GodotApiVersion {
pub major: u8,
pub minor: u8,
pub patch: u8,
pub version_string: String,
}
pub struct FunctionCommon {
pub name: String,
pub godot_name: String,
pub parameters: Vec<FnParam>,
pub return_value: FnReturn,
pub is_vararg: bool,
pub is_private: bool,
pub is_virtual_required: bool,
pub is_unsafe: bool,
pub direction: FnDirection,
pub deprecation_msg: Option<&'static str>,
}
pub trait Function: fmt::Display {
fn common(&self) -> &FunctionCommon;
fn qualifier(&self) -> FnQualifier;
fn surrounding_class(&self) -> Option<&TyName>;
fn name(&self) -> &str {
&self.common().name
}
fn name_ident(&self) -> Ident {
safe_ident(self.name())
}
fn godot_name(&self) -> &str {
&self.common().godot_name
}
fn params(&self) -> &[FnParam] {
&self.common().parameters
}
fn return_value(&self) -> &FnReturn {
&self.common().return_value
}
fn is_vararg(&self) -> bool {
self.common().is_vararg
}
fn is_private(&self) -> bool {
self.common().is_private
}
fn is_virtual(&self) -> bool {
matches!(self.direction(), FnDirection::Virtual { .. })
}
fn direction(&self) -> FnDirection {
self.common().direction
}
fn is_virtual_required(&self) -> bool {
self.common().is_virtual_required
}
fn is_builtin(&self) -> bool {
false
}
fn is_exposed_outer_builtin(&self) -> bool {
false
}
}
pub struct UtilityFunction {
pub common: FunctionCommon,
}
impl UtilityFunction {
pub fn hash(&self) -> i64 {
match self.direction() {
FnDirection::Virtual { .. } => unreachable!("utility function cannot be virtual"),
FnDirection::Outbound { hash } => hash,
}
}
}
impl Function for UtilityFunction {
fn common(&self) -> &FunctionCommon {
&self.common
}
fn qualifier(&self) -> FnQualifier {
FnQualifier::Global
}
fn surrounding_class(&self) -> Option<&TyName> {
None
}
}
impl fmt::Display for UtilityFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "utility function `{}`", self.name())
}
}
pub struct BuiltinMethod {
pub common: FunctionCommon,
pub qualifier: FnQualifier,
pub surrounding_class: TyName,
pub is_exposed_in_outer: bool,
}
impl BuiltinMethod {
pub fn hash(&self) -> i64 {
match self.direction() {
FnDirection::Virtual { .. } => unreachable!("builtin method cannot be virtual"),
FnDirection::Outbound { hash } => hash,
}
}
}
impl Function for BuiltinMethod {
fn common(&self) -> &FunctionCommon {
&self.common
}
fn qualifier(&self) -> FnQualifier {
self.qualifier
}
fn surrounding_class(&self) -> Option<&TyName> {
Some(&self.surrounding_class)
}
fn is_builtin(&self) -> bool {
true
}
fn is_exposed_outer_builtin(&self) -> bool {
self.is_exposed_in_outer
}
}
impl fmt::Display for BuiltinMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"builtin method `{}::{}`",
self.surrounding_class.rust_ty,
self.name(),
)
}
}
pub struct ClassMethod {
pub common: FunctionCommon,
pub qualifier: FnQualifier,
pub surrounding_class: TyName,
}
impl Function for ClassMethod {
fn common(&self) -> &FunctionCommon {
&self.common
}
fn qualifier(&self) -> FnQualifier {
self.qualifier
}
fn surrounding_class(&self) -> Option<&TyName> {
Some(&self.surrounding_class)
}
}
impl fmt::Display for ClassMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"class method `{}::{}`",
self.surrounding_class.rust_ty,
self.name(),
)
}
}
pub struct ClassSignal {
pub name: String,
pub parameters: Vec<FnParam>,
pub surrounding_class: TyName,
}
#[derive(Copy, Clone, Debug)]
pub enum FnDirection {
Virtual {
#[cfg(since_api = "4.4")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.4")))]
hash: u32,
},
Outbound { hash: i64 },
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) enum FlowDirection {
GodotToRust,
RustToGodot,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum FnQualifier {
Mut, Const, Static, Global, }
impl FnQualifier {
pub fn from_const_static(is_const: bool, is_static: bool) -> FnQualifier {
if is_static {
assert!(
!is_const,
"const and static qualifiers are mutually exclusive"
);
FnQualifier::Static
} else if is_const {
FnQualifier::Const
} else {
FnQualifier::Mut
}
}
pub fn is_static_or_global(&self) -> bool {
matches!(self, Self::Static | Self::Global)
}
}
pub struct FnParam {
pub name: Ident,
pub type_: RustTy,
pub default_value: Option<TokenStream>,
}
impl FnParam {
pub fn builder() -> FnParamBuilder {
FnParamBuilder::new()
}
}
pub(crate) struct FnParamBuilder {
replacements: EnumReplacements,
no_defaults: bool,
}
impl FnParamBuilder {
pub fn new() -> Self {
Self {
replacements: &[],
no_defaults: false,
}
}
pub fn enum_replacements(mut self, replacements: EnumReplacements) -> Self {
self.replacements = replacements;
self
}
#[expect(dead_code)] pub fn no_defaults(mut self) -> Self {
self.no_defaults = true;
self
}
#[expect(dead_code)] pub fn build_single(
self,
method_arg: &JsonMethodArg,
flow: FlowDirection,
ctx: &mut Context,
) -> FnParam {
self.build_single_impl(method_arg, flow, ctx)
}
pub fn build_many(
self,
method_args: &Option<Vec<JsonMethodArg>>,
flow: FlowDirection,
ctx: &mut Context,
) -> Vec<FnParam> {
option_as_slice(method_args)
.iter()
.map(|arg| self.build_single_impl(arg, flow, ctx))
.collect()
}
fn build_single_impl(
&self,
method_arg: &JsonMethodArg,
flow: FlowDirection,
ctx: &mut Context,
) -> FnParam {
let name = safe_ident(&method_arg.name);
let type_ =
conv::to_rust_type(&method_arg.type_, method_arg.meta.as_ref(), Some(flow), ctx);
let matching_replacement = self
.replacements
.iter()
.find(|(p, ..)| *p == method_arg.name);
let type_ = if let Some((_, enum_name, is_bitfield)) = matching_replacement {
if !type_.is_integer() {
panic!(
"Parameter `{}` is of type {}, but can only replace int with enum",
method_arg.name, type_
);
}
conv::to_enum_type_uncached(enum_name, *is_bitfield)
} else {
type_
};
let default_value = if self.no_defaults {
None
} else {
method_arg
.default_value
.as_ref()
.map(|v| conv::to_rust_expr(v, &type_))
};
FnParam {
name,
type_,
default_value,
}
}
}
impl fmt::Debug for FnParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let def_val = self
.default_value
.as_ref()
.map_or(String::new(), |v| format!(" (default {v})"));
write!(f, "{}: {}{}", self.name, self.type_, def_val)
}
}
pub struct FnReturn {
pub decl: TokenStream,
pub type_: Option<RustTy>,
}
impl FnReturn {
pub fn new(
return_value: &Option<JsonMethodReturn>,
flow: FlowDirection,
ctx: &mut Context,
) -> Self {
Self::with_enum_replacements(return_value, &[], flow, ctx)
}
pub fn with_enum_replacements(
return_value: &Option<JsonMethodReturn>,
replacements: EnumReplacements,
flow: FlowDirection,
ctx: &mut Context,
) -> Self {
if let Some(ret) = return_value {
let ty = conv::to_rust_type(&ret.type_, ret.meta.as_ref(), Some(flow), ctx);
let matching_replacement = replacements.iter().find(|(p, ..)| p.is_empty());
let ty = if let Some((_, enum_name, is_bitfield)) = matching_replacement {
if !ty.is_integer() {
panic!(
"Return type is of type {}, but can only replace int with enum",
ty
);
}
conv::to_enum_type_uncached(enum_name, *is_bitfield)
} else {
ty
};
Self {
decl: ty.return_decl(),
type_: Some(ty),
}
} else {
Self {
decl: TokenStream::new(),
type_: None,
}
}
}
pub fn type_tokens(&self) -> TokenStream {
match &self.type_ {
Some(ty) => ty.to_token_stream(),
_ => quote! { () },
}
}
pub fn call_result_decl(&self) -> TokenStream {
let ret = self.type_tokens();
quote! { -> Result<#ret, crate::meta::error::CallError> }
}
}
pub type EnumReplacements = &'static [(&'static str, &'static str, bool)];
#[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct GodotTy {
pub ty: String,
pub meta: Option<String>,
pub flow: Option<FlowDirection>,
}
#[derive(Clone, Debug)]
pub enum RustTy {
BuiltinIdent { ty: Ident, arg_passing: ArgPassing },
TypedArray {
tokens: TokenStream,
#[cfg(not(feature = "codegen-full"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "codegen-full"))))]
elem_class: Option<String>,
},
TypedDictionary {
tokens: TokenStream,
#[cfg(not(feature = "codegen-full"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "codegen-full"))))]
key_class: Option<String>,
#[cfg(not(feature = "codegen-full"))] #[cfg_attr(published_docs, doc(cfg(not(feature = "codegen-full"))))]
value_class: Option<String>,
},
EngineEnum {
tokens: TokenStream,
#[allow(dead_code)] surrounding_class: Option<String>,
is_bitfield: bool,
},
EngineClass {
gd_tokens: TokenStream,
impl_as_object_arg: TokenStream,
inner_class: Ident,
is_nullable: bool,
},
RawPointer { inner: Box<RustTy>, is_const: bool },
SysPointerType { tokens: TokenStream },
ExtenderReceiver { tokens: TokenStream },
}
impl RustTy {
pub fn param_decl(&self) -> TokenStream {
match self {
RustTy::EngineClass {
impl_as_object_arg, ..
} => impl_as_object_arg.clone(),
other => other.to_token_stream(),
}
}
pub fn return_decl(&self) -> TokenStream {
quote! { -> #self }
}
pub fn tokens_non_null(&self) -> TokenStream {
match self {
Self::EngineClass { gd_tokens, .. } => gd_tokens.clone(),
other => other.to_token_stream(),
}
}
pub fn is_integer(&self) -> bool {
let RustTy::BuiltinIdent { ty, .. } = self else {
return false;
};
matches!(
ty.to_string().as_str(),
"i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "isize" | "usize"
)
}
pub fn is_sys_pointer(&self) -> bool {
let RustTy::RawPointer { inner, .. } = self else {
return false;
};
matches!(**inner, RustTy::SysPointerType { .. })
}
}
impl ToTokens for RustTy {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
RustTy::BuiltinIdent { ty: ident, .. } => ident.to_tokens(tokens),
RustTy::TypedArray { tokens: path, .. } => path.to_tokens(tokens),
RustTy::TypedDictionary { tokens: path, .. } => path.to_tokens(tokens),
RustTy::EngineEnum { tokens: path, .. } => path.to_tokens(tokens),
RustTy::EngineClass {
is_nullable,
gd_tokens: path,
..
} => {
if *is_nullable {
quote! { Option<#path> }.to_tokens(tokens)
} else {
path.to_tokens(tokens)
}
}
RustTy::RawPointer {
inner,
is_const: true,
} => quote! { crate::meta::RawPtr<*const #inner> }.to_tokens(tokens),
RustTy::RawPointer {
inner,
is_const: false,
} => quote! { crate::meta::RawPtr<*mut #inner> }.to_tokens(tokens),
RustTy::SysPointerType { tokens: path } => path.to_tokens(tokens),
RustTy::ExtenderReceiver { tokens: path } => path.to_tokens(tokens),
}
}
}
impl fmt::Display for RustTy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_token_stream().to_string().replace(" ", ""))
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum ArgPassing {
ByValue,
ByRef,
ImplAsArg,
}
pub enum VirtualMethodPresence {
Inherit,
Override { is_required: bool },
Remove,
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct TyName {
pub godot_ty: String,
pub rust_ty: Ident,
}
impl TyName {
pub fn from_godot(godot_ty: &str) -> Self {
Self {
godot_ty: godot_ty.to_string(),
rust_ty: ident(&conv::to_pascal_case(godot_ty)),
}
}
pub fn from_godot_builtin(godot_ty: &JsonBuiltinClass) -> Self {
let godot_ty = godot_ty.name.as_str();
if godot_ty == "String" {
Self {
godot_ty: godot_ty.to_string(),
rust_ty: ident("GString"),
}
} else {
Self::from_godot(godot_ty)
}
}
pub fn description(&self) -> String {
if self.rust_ty == self.godot_ty {
self.godot_ty.clone()
} else {
format!("{} [renamed {}]", self.godot_ty, self.rust_ty)
}
}
pub fn virtual_trait_name(&self) -> String {
format!("I{}", self.rust_ty)
}
}
impl ToTokens for TyName {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.rust_ty.to_tokens(tokens)
}
}
#[derive(Clone)]
pub struct ModName {
pub rust_mod: Ident,
}
impl ModName {
pub fn from_godot(godot_ty: &str) -> Self {
let rust_mod = ident(&conv::to_snake_case(godot_ty));
Self { rust_mod }
}
pub fn from_godot_builtin(godot_ty: &JsonBuiltinClass) -> Self {
if godot_ty.name == "String" {
Self {
rust_mod: ident("gstring"),
}
} else {
Self::from_godot(&godot_ty.name)
}
}
}
impl ToTokens for ModName {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.rust_mod.to_tokens(tokens)
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum ClassCodegenLevel {
Core,
Servers,
Scene,
Editor,
}
impl ClassCodegenLevel {
pub fn with_tables() -> [Self; 4] {
[Self::Core, Self::Servers, Self::Scene, Self::Editor]
}
pub fn table_global_getter(self) -> Ident {
format_ident!("class_{}_api", self.lower())
}
pub fn table_file(self) -> String {
format!("table_{}_classes.rs", self.lower())
}
pub fn table_struct(self) -> Ident {
format_ident!("Class{}MethodTable", self.upper())
}
pub fn lower(self) -> &'static str {
match self {
Self::Core => "core",
Self::Servers => "servers",
Self::Scene => "scene",
Self::Editor => "editor",
}
}
fn upper(self) -> &'static str {
match self {
Self::Core => "Core",
Self::Servers => "Servers",
Self::Scene => "Scene",
Self::Editor => "Editor",
}
}
pub fn to_init_level(self) -> TokenStream {
match self {
Self::Core => quote! { crate::init::InitLevel::Core },
Self::Servers => quote! { crate::init::InitLevel::Servers },
Self::Scene => quote! { crate::init::InitLevel::Scene },
Self::Editor => quote! { crate::init::InitLevel::Editor },
}
}
}